Spine深入学习 —— 数据

atlas数据的处理

作用

图集,描述了spine使用的图片信息。

结构

page 页块

页块包含了页图像名称, 以及加载和渲染图像的相关信息。

txt 复制代码
page1.png
   size: 640, 480
   format: RGBA8888
   filter: Linear, Linear
   repeat: none
   pma: true
  • name: 首行为该页中的图像名称. 图片位置由atlas加载器来查找, 通常是再atlas文件的同目录下
  • size: 页中图像的宽度和高度. 在加载图像之前告知atlas加载器是非常有必要的, 例如, 可以提前为其分配缓冲区. 若省略则默认为0,0.
  • format: atlas加载器在内存中存储图像时应使用的格式. Atlas加载器可忽略该属性, 其可用值为: Alpha、Intensity、LuminanceAlpha、RGB565、RGBA4444、RGB888或RGBA8888. 若省略则默认为RGBA8888.
  • filter: Texture过滤器的缩略和放大方式. Atlas加载器可忽略该属性. 其可用值为: Nearest, Linear, MipMap, MipMapNearestNearest, MipMapLinearNearest, MipMapNearestLinear, 或MipMapLinearLinear. 若省略则默认为Nearest.
  • repeat: Texture包裹设置. Atlas加载器可忽略该属性. 其可用值为: x, y, xy, 或 none. 若省略则默认为none.
  • pma: 若值为true则表示图像使用了premultiplied alpha. 若省略则默认为false.
过滤方式 (OpenGL 纹理)

参考:https://learnopengl-cn.readthedocs.io/zh/latest/01 Getting started/06 Textures/

  • Nearest :对应GL的GL_NEAREST ,邻近过滤。是OpenGL默认的纹理过滤方式。会选择中心点最接近纹理坐标的那个像素

加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色

  • Linear :对应GL的GL_LINEAR,线性过滤。会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色

    一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大

具体区别如下图

GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR可以产生更真实的输出。

以下是多级渐远纹理

问题:假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了
解决:OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。

  • MipMapNearestNearest :对应GL的GL_NEAREST_MIPMAP_NEAREST。使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样。
  • MipMapLinearNearest :对应GL的GL_LINEAR_MIPMAP_NEAREST。使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
  • MipMapNearestLinear :对应GL的GL_NEAREST_MIPMAP_LINEAR。在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
  • MipMapLinearLinear :对应GL的GL_LINEAR_MIPMAP_LINEAR。在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
premultiplied alpha

参考:https://segmentfault.com/a/1190000002990030

Alpha通道的工作原理: 最常见的像素表示格式是RGBA8888即 (r, g, b, a),每个通道8位,0-255。例如红色60%透明度就是 (255, 0, 0, 153),为了表示方便alpha通道一般记成正规化后的0-1的浮点数,也就是 (255, 0, 0, 0.6)

Premultiplied Alpha 则是把RGB通道乘以透明度也就是 (r * a, g * a, b * a, a),50%透明红色就变成了(153, 0, 0, 0.6)。

透明通道在渲染的时候通过 Alpha Blending 产生作用,如果一个透明度为 a s a_s as的颜色 C s C_s Cs渲染到颜色 C d C_d Cd上,混合后的颜色如下:

C o = α s C s + ( 1 − α s ) C d C_o = \alpha_{s} C_s + (1 - \alpha_{s})C_d Co=αsCs+(1−αs)Cd

以60%透明的红色渲染到白色背景为例:

C o = ( 255 , 0 , 0 ) ⋅ 0.6 + ( 255 , 255 , 255 ) ⋅ ( 1 − 0.6 ) = ( 255 , 102 , 102 ) C_o = (255,0,0) \cdot 0.6 + (255,255,255) \cdot (1 - 0.6) = (255,102,102) Co=(255,0,0)⋅0.6+(255,255,255)⋅(1−0.6)=(255,102,102)

如果按照Premultiplied Alpha存储那么实际上的 α s C s \alpha_{s}C_s αsCs已经计算过了一次。

Premultiplied Alpha 之后,混合的时候可以少一次乘法,这可以提高一些效率,但这并不是最主要的原因。

最主要的原因是:

没有 Premultiplied Alpha 的纹理无法进行 Texture Filtering(除非使用最近邻插值)。

我们使用的PNG图片纹理,一般是不会 Premultiplied Alpha 的。游戏引擎在载入PNG纹理后回手动处理,然后再glTexImage2D传给GPU

比如 Cocos2D-x 中的 CCImage::premultipliedAlpha:

cpp 复制代码
void Image::premultipliedAlpha() {
    unsigned int* fourBytes = (unsigned int*)_data;
    for (int i = 0; i < _width * _height; i++) {
        unsigned char* p = _data + i * 4;
        fourBytes[i] = CC_RGB_PREMULTIPLY_ALPHA(p[0], p[1], p[2], p[3]);
    }  
    _hasPremultipliedAlpha = true;
}

而GPU专用的纹理格式,比如 PVR、ETC 一般在生成纹理都是默认 Premultiplied Alpha 的,这些格式一般是GPU硬解码,引擎用CPU处理会很慢。

总之 glTexImage2D 传给 GPU 的纹理数据最好都是 Multiplied Alpha 的,要么在生成纹理时由纹理工具 Pre-multiplied,要么载入纹理后由游戏引擎或UI框架 Post-multiplied。

区域块 Region

区域块包含了页图像中的区域位置以及该区域的其他信息

txt 复制代码
bg-dialog
   index: -1
   rotate: false
   bounds: 519, 223, 17, 38
   offsets: 2, 2, 21, 42
   split: 10, 10, 29, 10
   pad: -1, -1, 28, 10

L1/L1_01
  rotate: true
  xy: 1855, 118
  size: 73, 78
  orig: 211, 216
  offset: 69, 69
  index: -1
  • name: 首行为区域名称, 用于在atlas中定位一个区域. 多个区域若索引(index)各不相同, 则它们可以同名.
  • index: 索引可以打包许多同名图像, 只要每个图像索引不同即可. 通常情况下, 索引是区域的帧号, 这些区域在逐帧动画中会依序绘制. 若省略则默认为-1.
  • bounds : 该图像在页图像中的像素位置x和y, 以及打包后图像尺寸, 即该图像在页图像中的像素尺寸. 若省略则默认为0,0,0,0.有些会以xy、size的方式记录
  • offsets: 在打包前, 要从图像的左侧和底部边缘去除的空白像素值, 以及原始图像尺寸(此图像在打包前的像素尺寸). 如果进行了去除操作, 则打包后的图像尺寸可能会小于原始图像. 若省略则左侧和底部像素偏移默认为0,0, 原始图像尺寸等于打包后的图像尺寸.
  • rotate: 若为true, 则表示该区域被逆时针旋转90度后存储在页图像中; 若为false则表示没有旋转. 属性值也可直接填入旋转角度(范围是0至360度). 若省略则默认为0.
  • split: 对图像进行九宫格分割, 属性值是从原图边缘算起的像素值. 分割所定义的3x3的网格, 可以在缩放图像时不用拉伸图像的所有部分. 若省略则默认为null.
  • pad: 九宫格分割后四周的填充厚度, 属性值是从原图边缘算起的像素值. 填充可以把置于九宫格中的内容以不同的方式嵌入到分片中. 若省略则默认为null.

数据结构

以下都以cocos2dx源码作为说明。

由上面可以知道,一个atlas文件由两个部分组成:Page和Region。

那么对于一个Atlas结构:

cpp 复制代码
struct spAtlas {
	spAtlasPage* pages;
	spAtlasRegion* regions;

	void* rendererObject;
};

其中spAtlasPage如下:

cpp 复制代码
struct spAtlasPage {
	const spAtlas* atlas;
	const char* name;
	spAtlasFormat format;
	spAtlasFilter minFilter, magFilter;
	spAtlasWrap uWrap, vWrap;

	void* rendererObject;
	int width, height;

	spAtlasPage* next;
};

其中spAtlasFormat对应的就是数据中的format,其结构为

cpp 复制代码
typedef enum {
	SP_ATLAS_UNKNOWN_FORMAT,
	SP_ATLAS_ALPHA,
	SP_ATLAS_INTENSITY,
	SP_ATLAS_LUMINANCE_ALPHA,
	SP_ATLAS_RGB565,
	SP_ATLAS_RGBA4444,
	SP_ATLAS_RGB888,
	SP_ATLAS_RGBA8888
} spAtlasFormat;

spAtlasFilter对应filter

cpp 复制代码
typedef enum {
	SP_ATLAS_UNKNOWN_FILTER,
	SP_ATLAS_NEAREST,
	SP_ATLAS_LINEAR,
	SP_ATLAS_MIPMAP,
	SP_ATLAS_MIPMAP_NEAREST_NEAREST,
	SP_ATLAS_MIPMAP_LINEAR_NEAREST,
	SP_ATLAS_MIPMAP_NEAREST_LINEAR,
	SP_ATLAS_MIPMAP_LINEAR_LINEAR
} spAtlasFilter;

spAtlasWrap对应split

cpp 复制代码
typedef enum {
	SP_ATLAS_MIRROREDREPEAT,
	SP_ATLAS_CLAMPTOEDGE,
	SP_ATLAS_REPEAT
} spAtlasWrap;

最后的 spAtlasPage* next; 表示结构是用一个链表的方式存储。

在看看区域块Region的数据结构。

cpp 复制代码
struct spAtlasRegion {
	const char* name;
	int x, y, width, height;
	float u, v, u2, v2;
	int offsetX, offsetY;
	int originalWidth, originalHeight;
	int index;
	int/*bool*/rotate;
	int/*bool*/flip;
	int* splits;
	int* pads;

	spAtlasPage* page;

	spAtlasRegion* next;
};

读取

读取函数为spAtlas_createFromFile

大概实现如下

cpp 复制代码
spAtlas* spAtlas_createFromFile(const char* path, void* rendererObject) {
	int dirLength;
	char *dir;
	int length;
	const char* data;

	spAtlas* atlas = 0;

	/* Get directory from atlas path. */
	const char* lastForwardSlash = strrchr(path, '/');
	const char* lastBackwardSlash = strrchr(path, '\\');
	const char* lastSlash = lastForwardSlash > lastBackwardSlash ? lastForwardSlash : lastBackwardSlash;
	if (lastSlash == path) lastSlash++; /* Never drop starting slash. */
	dirLength = (int)(lastSlash ? lastSlash - path : 0);
	dir = MALLOC(char, dirLength + 1);
	memcpy(dir, path, dirLength);
	dir[dirLength] = '\0';
    
   //上面在处理路径,这里才是读取atlas文件。
	data = _spUtil_readFile(path, &length);
	//将文件流转换成spAtlas结构的对象。
	if (data) atlas = spAtlas_create(data, length, dir, rendererObject);

	FREE(data);
	FREE(dir);
	return atlas;
}

//读取文件,返回文件二进制数据和长度
char* _spUtil_readFile (const char* path, int* length) {
	Data data = FileUtils::getInstance()->getDataFromFile(FileUtils::getInstance()->fullPathForFilename(path));
	if (data.isNull()) return 0;

	// avoid buffer overflow (int is shorter than ssize_t in certain platforms)
#if COCOS2D_VERSION >= 0x00031200
	ssize_t tmpLen;
	char *ret = (char*)data.takeBuffer(&tmpLen);
	*length = static_cast<int>(tmpLen);
	return ret;
#else
    *length = static_cast<int>(data.getSize());
    char* bytes = MALLOC(char, *length);
    memcpy(bytes, data.getBytes(), *length);
    return bytes;
#endif
}

函数spAtlas_create 很长,大概作用就是对读出来的文件流进行解析,解析的格式按照
spAtlasPagespAtlasRegion 结构体来。返回spAtlas对象。

对于spAtlas 上的rendererObject传入都是0,对于spAtlasPage上rendererObject是保存page中的name属性对应的图片的纹理。具体创建是

cpp 复制代码
void _spAtlasPage_createTexture (spAtlasPage* self, const char* path) {
	Texture2D* texture = Director::getInstance()->getTextureCache()->addImage(path);
	CCASSERT(texture != nullptr, "Invalid image");
	texture->retain();

	Texture2D::TexParams textureParams = {filter(self->minFilter), filter(self->magFilter), wrap(self->uWrap), wrap(self->vWrap)};
	texture->setTexParameters(textureParams);

	self->rendererObject = texture;
	self->width = texture->getPixelsWide();
	self->height = texture->getPixelsHigh();
}

目的是缓存纹理,提升速度。

使用

spAtlas 被创建出来之后,会调用Cocos2dAttachmentLoader_create 方法将spAtlas 对象转换成spAttachmentLoader对象。

根据Spine官方文档中指出

创建SkeletonJson或SkeletonBinary实例需要指定一个AttachmentLoader,它包含返回新附件实例的方法。AttachmentLoader提供了一个加载时自定义附件的方式,如设置附件的图集区域以便稍后渲染。这个很常用,所以Spine运行时自带有一个功能完全相同的AtlasAttachmentLoader

cpp 复制代码
Cocos2dAttachmentLoader* Cocos2dAttachmentLoader_create (spAtlas* atlas) {
	Cocos2dAttachmentLoader* self = NEW(Cocos2dAttachmentLoader);
	_spAttachmentLoader_init(SUPER(self), _Cocos2dAttachmentLoader_dispose, _Cocos2dAttachmentLoader_createAttachment,
		_Cocos2dAttachmentLoader_configureAttachment, _Cocos2dAttachmentLoader_disposeAttachment);
	self->atlasAttachmentLoader = spAtlasAttachmentLoader_create(atlas);
	return self;
}

spAtlasAttachmentLoader* spAtlasAttachmentLoader_create (spAtlas* atlas) {
	spAtlasAttachmentLoader* self = NEW(spAtlasAttachmentLoader);
	_spAttachmentLoader_init(SUPER(self), _spAttachmentLoader_deinit, _spAtlasAttachmentLoader_createAttachment, 0, 0);
	self->atlas = atlas;
	return self;
}

对于 spAtlasAttachmentLoader 其中包含了spAtlas ,不过添加了spAttachmentLoader的对象。

cpp 复制代码
typedef struct spAtlasAttachmentLoader {
	spAttachmentLoader super;
	spAtlas* atlas;
} spAtlasAttachmentLoader;

typedef struct spAttachmentLoader {
	const char* error1;
	const char* error2;

	const void* const vtable;
#ifdef __cplusplus
	spAttachmentLoader () :
					error1(0),
					error2(0),
					vtable(0) {
	}
#endif
} spAttachmentLoader;

spAtlasAttachmentLoader_create创建了spAtlasAttachmentLoader 对象,直接保存spAtlas 对象,关键是 _spAttachmentLoader_init 函数,会去创建spAttachmentLoader 对象,该实现有值得学习的思路。

cpp 复制代码
void _spAttachmentLoader_init (spAttachmentLoader* self,
	void (*dispose) (spAttachmentLoader* self),
	spAttachment* (*createAttachment) (spAttachmentLoader* self, spSkin* skin, spAttachmentType type, const char* name,
		const char* path),
	void (*configureAttachment) (spAttachmentLoader* self, spAttachment*),
	void (*disposeAttachment) (spAttachmentLoader* self, spAttachment*)
) {
	CONST_CAST(_spAttachmentLoaderVtable*, self->vtable) = NEW(_spAttachmentLoaderVtable);
	VTABLE(spAttachmentLoader, self)->dispose = dispose;
	VTABLE(spAttachmentLoader, self)->createAttachment = createAttachment;
	VTABLE(spAttachmentLoader, self)->configureAttachment = configureAttachment;
	VTABLE(spAttachmentLoader, self)->disposeAttachment = disposeAttachment;
}

他会去构建dispose、createAttachment、configureAttachment、disposeAttachment到结构体中,这样,可以直接通过结构体去调用该方法,每个结构体创建的时候可以传入不同的函数。

简单来说,spAtlasAttachmentLoader 就是把spAtlas对象和其他需要用的方法以附件的方式加载到结构体中。

最后Cocos2dAttachmentLoader会配合JSON或者Binary文件来创建Spine。

cocos2dx-lua的改进

从源码上来看,对于Altals文件会IO从二进制,最后再解析从spAtlas对象。

创建也可以直接用spAtlas对象创建,这里减少对文件的IO。

对于lua的导出这里并没有实现的方法,估计是对spAtlas结构体的导出没有实现。

这里思路可以建立一个缓存机制,创建一次Spine,将其所有保存下来,下一次创建的时候,先检查缓存中是否有,如果有的话,那么就读缓存。

JSON和Binary

JSON

SkeletonJson,其好处是可读,缺点是文件较大,解析慢。

Binary

SkeletonBinary,不可读,但是加载速度快,文件小。

JSON数据

参考:https://zh.esotericsoftware.com/spine-json-format

Skeleton
json 复制代码
"skeleton": {
   "hash": "5WtEfO08B0TzTg2mDqj4IHYpUZ4",
   "spine": "3.8.24",
   "x": -17.2,
   "y": -13.3,
   "width": 470.86,
   "height": 731.44,
   "images": "./images/",
   "audio": "./audio/"
},
  • hash: 所有skeleton数据的哈希值. 工具软件用它来检测Skeleton数据自上次加载后是否有变化.
  • version: 导出数据的Spine版本. 工具软件用它将Skeleton数据限制于某个特定的Spine版本.
  • x: skeleton附件AABB(axis-aligned bounding box)包围盒左下角点的X坐标, 与Spine中的setup pose相同.
  • y: skeleton附件AABB包围盒左下角点的Y坐标, 与Spine中的setup pose相同.
  • width: skeleton附件AABB包围盒宽度, 与Spine中的setup pose相同. 虽然skeleton的AABB盒取决于其pose, 但也可作为skeleton的大体尺寸.
  • height: skeleton附件AABB包围盒高度, 与Spine中的setup pose相同.
  • fps: Dopesheet(摄影表)的帧率, 单位为帧数/秒, 与Spine中相同. 若省略则默认为30. 非必要数据.
  • images: 图像文件路径, 与Spine中相同. 非必要数据.
  • audio: 音频文件路径, 与Spine中相同. 非必要数据.
骨骼(Bones)

导出文件的bones部分存储了setup pose状态的骨骼数据.

json 复制代码
"bones": [
   { "name": "root" },
   { "name": "torso", "parent": "root", "length": 85.82, "x": -6.42, "y": 1.97, "rotation": 94.95 },
   ...
],

骨骼是有序的, 父骨骼总是排在子骨骼之前.

  • name: 骨骼名称, 该名称在skeleton上唯一.
  • length: 骨骼长度. 运行时通常不使用骨骼长度属性, 除非需要为骨骼绘制调试线. 若省略则默认为0.
  • transform: 该属性决定子骨骼以何种方式继承父骨骼的变换: normal, onlyTranslation, noRotationOrReflection, noScale或noScaleOrReflection. 若省略则默认为normal.
  • skin: 若为true, 则只有当活动皮肤包含该骨骼时该骨骼才算活动. 若省略则默认为false.
  • x: setup pose时骨骼相对于父对象位置的X值. 若省略则默认为0.
  • y: setup pose时骨骼相对于父对象位置的Y值. 若省略则默认为0.
  • rotation: setup pose时骨骼相对于父对象的旋转角度. 若省略则默认为0.
  • scaleX: setup pose时骨骼X方向的缩放比例. 若省略则默认为1.
  • scaleY: setup pose时骨骼Y方向的缩放比例. 若省略则默认为1.
  • shearX: setup pose时骨骼X方向的斜切角度. 若省略则默认为0.
  • shearY: setup pose时骨骼Y方向的斜切角度. 若省略则默认为0.
  • color: 骨骼的颜色, 与Spine中相同. 若省略则默认为0x989898FF RGBA. 非必要数据.
槽位(Slots)

槽位部分描述了绘制顺序和可分配附件的可用槽位.

json 复制代码
"slots": [
   { "name": "left shoulder", "bone": "left shoulder", "attachment": "left-shoulder" },
   { "name": "left arm", "bone": "left arm", "attachment": "left-arm" },
   ...
],

如果没有槽位, "slots"部分可被省略. 槽位是按照setup pose的绘制顺序排序的. 索引较高的槽位中的图像被绘制在索引较低的槽位的图像之上.

  • name: 槽位名称. 该名称在skeleton上唯一.
  • bone: 该槽位所在骨骼的名称.
  • color: setup pose时槽位的颜色. 它是一个长度为8的字符串, 包含4个按RGBA顺序排列的两位十六进制数字. 若省略alpha, 则alpha默认为 "FF". 若省略该属性则默认为 "FFFFFFF".
  • dark: 设置setup pose时槽位用于双色tinting的dark color. 这是一个6个字符的字符串, 包含3个按RGB顺序排列的两位十六进制数字. 当不使用双色tinting时则省略.
  • attachment: setup pose时槽位中附件的名称. 若省略则默认setup pose没有附件.
  • blend: 在绘制槽位中可见附件时要使用的blend类别: normal, additive, multiply, 或screen.
约束(Constraints)
IK约束

IK约束用于设置骨骼旋转,可使骨骼末梢接触或指向目标骨骼。这有多种用途,但最常见的是通过移动手或脚来控制四肢。

json 复制代码
"ik": [
   {
      "name": "left leg",
      "order": 2,
      "bones": [ "left thigh", "left shin" ],
      "target": "left ankle",
      "mix": 0.5,
      "bendPositive": false,
      "compress": true,
   },
   ...
],

如果没有IK约束, "ik"部分可被省略.

  • name: 约束名称. 该名称在skeleton上唯一.
  • order: 约束生效(applied)的顺序序数.
  • skin: 若为true, 则只有当活动皮肤包含该约束时该约束才生效. 若省略则默认为false.
  • bones: 一个包含1到2个骨骼名称的列表, 这些骨骼的旋转将被IK约束限制.
  • target: 目标(target)骨骼的名称.
  • mix: 一个介于0到1区间的值, 表示约束对骨骼的影响, 其中0表示只有FK, 1表示只有IK, 而中间值表示混合了FK和IK. 若省略则默认为1.
  • *softness: 对于双骨骼IK, 表示目标骨骼到旋转减缓前骨骼的最大活动范围的距离. 若省略则默认为0.
  • bendPositive: 若为true, 则骨骼的弯曲方向为正的旋转方向. 若省略则默认为false.
  • compress: 若为true且只有一个骨骼被约束, 则当目标太近时会缩放骨骼以保持连接. 若省略则默认为false.
  • stretch: 若为true且如果目标超出了范围, 将缩放父骨骼以保持连接. 若约束了多个骨骼且父骨骼的局部缩放比例非均匀(nonuniform), 则不应用拉伸(stretch). 若省略则默认为false.
  • uniform: 若为true且只约束了一个骨骼, 而且使用了压缩或拉伸, 则该骨骼将在X和Y方向上缩放. 若省略则默认为false.
Transform约束

变换约束可将骨骼的世界旋转、平移、缩放和/或剪切(其变换)复制到一个或多个其他骨骼

变换约束对高级装配有许多巧妙的用途。最简单的方法是移动一个骨骼,然后让其他骨骼也移动。它可用于模拟有不同父对象的骨骼,例如摘下帽子、装备武器或抛出物体。可以通过仅约束变换的子集(例如,仅限制缩放或剪切)来创建有趣的效果。可以将一个骨骼自动放置在其他两个骨骼之间距离的任意百分比等等。

json 复制代码
"transform": [
   { "name": "weapon to hip", "order": 1, "bone": "weapon", "target": "hip" },
   ...
],

如果没有变换约束, "transform"部分可被省略.

  • name: 约束名称. 该名称在skeleton上唯一.
  • order: 约束生效(applied)的顺序序数.
  • skin: 若为true, 则只有当活动皮肤包含该约束时该约束才生效. 若省略则默认为false.
  • bones: 将被约束控制transform的骨骼.
  • target: 目标(target)骨骼的名称.
  • rotation: 相对于目标骨骼的旋转角度偏移量. 若省略则默认为0.
  • x: 相对于目标骨骼的X方向距离偏移量. 若省略则默认为0.
  • y: 相对于目标骨骼的Y方向距离偏移量. 若省略则默认为0.
  • scaleX: 相对于目标骨骼的X方向缩放偏移量. 若省略则默认为0.
  • scaleY: 相对于目标骨骼的Y方向缩放偏移量. 若省略则默认为0.
  • shearY: 相对于目标骨骼的Y方向斜切角度偏移量. 若省略则默认为0.
  • rotateMix: 一个介于0到1区间的值, 表示约束对骨骼的影响, 其中0表示无影响, 1表示只有约束, 而中间值表示正常pose和约束的混合. 若省略则默认为1.
  • translateMix: 参见 rotateMix.
  • scaleMix: 参见 rotateMix.
  • shearMix: 参见 rotateMix.
  • local: 如果需要影响目标的局部transform则设置为True, 反之则影响全局transform. 若省略则默认为false.
  • relative: 如果目标的transform为相对的则设置为True, 反之则其transform为绝对的. 若省略则默认为false.
Path约束

路径约束使用路径调整骨骼变换。骨骼可以沿路径平移,并将其旋转调整为指向路径。

路径约束可以替换平移关键帧,从而可以通过使用路径更轻松地定义移动。许多其他用途包括将多个骨骼约束到一条路径,然后通过操纵路径来控制骨骼,而非单独调整每个骨骼。例如,骨骼可以沿路径均匀分布,也可以放大,使它们看起来像是沿着路径生长。

json 复制代码
"path": [
   {
      "name": "constraintName",
      "order": 0,
      "bones": [ "boneName1", "boneName2" ],
      "target": "slotName",
      "positionMode": "fixed",
      "spacingMode": "length",
      "rotateMode": "tangent",
      "rotation": "45",
      "position": "204",
      "spacing": "10",
      "rotateMix": "0",
      "translateMix": "1"
   },
   ...
],

如果没有路径约束, "path"部分可被省略.

  • name: 约束名称. 该名称在skeleton上唯一.
  • order: 约束生效(applied)的顺序序数.
  • skin: 若为true, 则只有当活动皮肤包含该约束时该约束才生效. 若省略则默认为false.
  • bones: 将被约束控制旋转或平移的骨骼.
  • target: 目标(target)骨骼的名称.
  • positionMode: 指定计算路径位置的方式: 定值(fixed)或百分比(percent). 若省略则默认为percent.
  • spacingMode: 指定骨骼间间距的计算方式: 长度(length), 定值(fixed)或百分比(percent). 若省略则默认为length.
  • rotateMode: 决定骨骼旋转的计算方式: 切线(tangent)、链式或链式缩放. 若省略则默认为tangent.
  • rotation: 相对于路径的旋转偏移量. 若省略则默认为0.
  • position: 路径位置. 若省略则默认为0.
  • spacing: 骨骼间间距. 若省略则默认为0.
  • rotateMix: 一个介于0到1区间的值, 表示约束对骨骼的影响, 其中0表示无影响, 1表示只有约束, 而中间值表示正常pose和约束的混合. 若省略则默认为1.
  • translateMix: 参见 rotateMix.
Skins(皮肤)
json 复制代码
"skins": [
   {
      "name": "skinName",
      "attachments": {
         "slotName": {
            "attachmentName": { "x": -4.83, "y": 10.62, "width": 63, "height": 47 },
            ...
         },
         ...
      }
   },
   {
      "name": "skinName",
      "attachments": {
         "slotName": {
            "attachmentName": { "name": "actualAttachmentName", "x": 53.94, "y": -5.75, "rotation": -86.9, "width": 121, "height": 132 },
            ...
         },
         ...
      }
   },
   ...
],

如果没有皮肤或附件, "skins"部分可被省略.

每个皮肤本质上是一个映射(map), 键名是槽位和附件名称的复合键名, 其键值为一个附件. 虽然键中使用的附件名称对每个皮肤都相同, 但附件的实际附件名可能会有所不同.

当一个skeleton需要找到一个槽位中的一个附件时, 它首先会检查其皮肤. 如果没有找到, skeleton就会检查其默认皮肤. 默认皮肤包含没有被其他皮肤包含的附件, Spine可以混合使用皮肤和非皮肤附件. 默认皮肤总是带有"default"这个字样.

其他皮肤可能包含骨骼和/或约束:

json 复制代码
"skins": [
   {
      "name": "skinName",
      "bones": [ "bone1", "bone2" ],
      "ik": [ "ik1", "ik2" ],
      "transform": [ "transform1", "transform2" ],
      "path": [ "path1", "path2" ],
      "attachments": {
         ...
      }
   },
   ...
}
附件(Attachments)

每个附件的属性根据附件类型的不同而不同.

事件(Events)

事件部分描述了在动画过程中可以触发的命名事件及其setup pose值.

json 复制代码
"events" [
   "name": { "int": 1, "float": 2, "string": "three" },
   "name": { "int": 1, "float": 2, "string": "three", "audio": "hit.wav", "volume": 0.9, "balance": -0.25 },
   ...
],
动画(Animations)

一个动画包含一个时间轴列表. 每条时间轴均存储了一个关键帧列表, 这些时间轴描述了骨骼或槽位的值如何随时间变化.

json 复制代码
"animations": {
   "name": {
      "bones": { ... },
      "slots": { ... },
      "ik": { ... },
      "deform": { ... },
      "events": { ... },
      "draworder": { ... },
   },
   ...
}
骨骼时间轴

动画的"bones"部分描述了控制骨骼的时间轴.

json 复制代码
{
"bones": {
   "boneName": {
      "timelineType": [
         { "time": 0, "angle": -26.55 },
         { "time": 0.1333, "angle": -8.78 },
         ...
      ],
      ...
   },
   ...
},

每个关键帧的属性依时间轴的类型不同而变化.

槽位时间轴

动画的"slots"部分描述了操作槽位时间轴.

json 复制代码
"slots": {
   "slotName": {
      "timelineType": [
         { "time": 0.2333, "name": "eyes closed" },
         { "time": 0.6333, "name": "eyes open" },
         ...
      ],
      ...
   },
   ...
}

每个关键帧的属性依时间轴的类型不同而变化.

IK约束时间轴

动画的"ik"部分描述了IK约束时间轴.

json 复制代码
"ik": {
   "constraintName": [
      { "time": 0.5333, "mix": 0.616, "bendPositive": true },
      ...
   ],
   ...
},
Path约束时间轴

动画的"path"部分描述了路径约束时间轴.

json 复制代码
"path": {
   "constraintName": 
      "position": [
         { "time": 1.81, "position": 0.7 },
         ...
      ],
      "spacing": [
         { "time": 2.92, "spacing": 0.05 },
         ...
      ],
      "mix": [
         { "time": 3.03, "rotateMix": 0.5, "translateMix": 0.75 },
         ...
      ],
   ...
},
Deform时间轴

动画的"deform"部分描述了用于操作每个网格附件的顶点(网格变形)的时间轴.

json 复制代码
"deform": {
   "skinName": {
      "slotName": {
         "meshName": [
            {
               "time": 0,
               "curve": [ 0.25, 0, 0.75, 1 ]
            },
            {
               "time": 1.5,
               "offset": 12,
               "vertices": [ -0.75588, -3.68987, -1.01898, -2.97404, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
               -1.01898, -2.97404, -0.75588, -3.68987, 0, 0, -0.75588, -3.68987, -0.75588, -3.68987,
               -1.01898, -2.97404, -1.01898, -2.97404, -1.01898, -2.97404, -0.75588, -3.68987 ],
               "curve": [ 0.25, 0, 0.75, 1 ]
            },
            ...
         ],
         ...
      },
      ...
   },
   ...
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Event时间轴

动画的"events"部分描述了一个用于触发事件的时间轴.

json 复制代码
"events": [
   { "time": 0.2, "name": "event1", "int": 1, "float": 2, "string": "three" },
   { "time": 0.6, "name": "event2", "int": 4, "float": 5, "string": "six" },
   ...
}
绘制顺序(Draw order)时间轴

动画的"draworder"部分描述了一个用于改变绘制顺序的时间轴, 它是一个skeleton上需绘制附件的槽位列表.

json 复制代码
"draworder": [
   {
      "time": 0.2,
      "offsets": [
         { "slot": "slotName", "offset": 1 },
         { "slot": "slotName", "offset": -2 },
         ...
      ]
   },
   ...
}

二进制数据

参考:https://zh.esotericsoftware.com/spine-binary-format#格式

读取Json

cpp 复制代码
typedef struct spSkeletonJson {
	float scale;
	spAttachmentLoader* attachmentLoader;
	const char* const error;
} spSkeletonJson;

spSkeletonJson本质上就是spAttachmentLoader的组合,加上一个缩放属性。

cpp 复制代码
spSkeletonData* spSkeletonJson_readSkeletonDataFile (spSkeletonJson* self, const char* path) {
	int length;
	spSkeletonData* skeletonData;
	const char* json = _spUtil_readFile(path, &length);
	if (length == 0 || !json) {
		_spSkeletonJson_setError(self, 0, "Unable to read skeleton file: ", path);
		return 0;
	}
	skeletonData = spSkeletonJson_readSkeletonData(self, json);
	FREE(json);
	return skeletonData;
}

函数spSkeletonJson_readSkeletonData将json文件序列化spSkeletonData对象,即二进制对象。

cpp 复制代码
typedef struct spSkeletonData {
	const char* version;
	const char* hash;
	float width, height;

	int bonesCount;
	spBoneData** bones;

	int slotsCount;
	spSlotData** slots;

	int skinsCount;
	spSkin** skins;
	spSkin* defaultSkin;

	int eventsCount;
	spEventData** events;

	int animationsCount;
	spAnimation** animations;

	int ikConstraintsCount;
	spIkConstraintData** ikConstraints;

	int transformConstraintsCount;
	spTransformConstraintData** transformConstraints;

	int pathConstraintsCount;
	spPathConstraintData** pathConstraints;
} spSkeletonData;

spBoneData骨骼的信息

cpp 复制代码
typedef enum {
	SP_TRANSFORMMODE_NORMAL,
	SP_TRANSFORMMODE_ONLYTRANSLATION,
	SP_TRANSFORMMODE_NOROTATIONORREFLECTION,
	SP_TRANSFORMMODE_NOSCALE,
	SP_TRANSFORMMODE_NOSCALEORREFLECTION
} spTransformMode;

typedef struct spBoneData spBoneData;
struct spBoneData {
	const int index;
	const char* const name;
	spBoneData* const parent;
	float length;
	float x, y, rotation, scaleX, scaleY, shearX, shearY;
	spTransformMode transformMode;

#ifdef __cplusplus
	spBoneData() :
		index(0),
		name(0),
		parent(0),
		length(0),
		x(0), y(0),
		rotation(0),
		scaleX(0), scaleY(0),
		shearX(0), shearY(0),
		transformMode(SP_TRANSFORMMODE_NORMAL) {
	}
#endif
};

spSlotData插槽信息

cpp 复制代码
typedef enum {
	SP_BLEND_MODE_NORMAL, SP_BLEND_MODE_ADDITIVE, SP_BLEND_MODE_MULTIPLY, SP_BLEND_MODE_SCREEN
} spBlendMode;

typedef struct spSlotData {
	const int index;
	const char* const name;
	const spBoneData* const boneData;
	const char* attachmentName;
	spColor color;
	spColor* darkColor;
	spBlendMode blendMode;

#ifdef __cplusplus
	spSlotData() :
		index(0),
		name(0),
		boneData(0),
		attachmentName(0),
		color(),
		darkColor(0),
		blendMode(SP_BLEND_MODE_NORMAL) {
	}
#endif
} spSlotData;

spSkin皮肤

json 复制代码
typedef struct spSkin {
	const char* const name;

#ifdef __cplusplus
	spSkin() :
		name(0) {
	}
#endif
} spSkin;

spEventData事件数据

cpp 复制代码
typedef struct spEventData {
	const char* const name;
	int intValue;
	float floatValue;
	const char* stringValue;

#ifdef __cplusplus
	spEventData() :
		name(0),
		intValue(0),
		floatValue(0),
		stringValue(0) {
	}
#endif
} spEventData;

spAnimation时间线数据

cpp 复制代码
typedef struct spAnimation {
	const char* const name;
	float duration;

	int timelinesCount;
	spTimeline** timelines;

#ifdef __cplusplus
	spAnimation() :
		name(0),
		duration(0),
		timelinesCount(0),
		timelines(0) {
	}
#endif
} spAnimation;

spIkConstraintData IK约束数据

cpp 复制代码
typedef struct spIkConstraintData {
	const char* const name;
	int order;
	int bonesCount;
	spBoneData** bones;

	spBoneData* target;
	int bendDirection;
	float mix;

#ifdef __cplusplus
	spIkConstraintData() :
		name(0),
		bonesCount(0),
		bones(0),
		target(0),
		bendDirection(0),
		mix(0) {
	}
#endif
} spIkConstraintData;

spTransformConstraintDataTransform约束数据

cpp 复制代码
typedef struct spTransformConstraintData {
	const char* const name;
	int order;
	int bonesCount;
	spBoneData** const bones;
	spBoneData* target;
	float rotateMix, translateMix, scaleMix, shearMix;
	float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
	int /*boolean*/ relative;
	int /*boolean*/ local;

#ifdef __cplusplus
	spTransformConstraintData() :
		name(0),
		bonesCount(0),
		bones(0),
		target(0),
		rotateMix(0),
		translateMix(0),
		scaleMix(0),
		shearMix(0),
		offsetRotation(0),
		offsetX(0),
		offsetY(0),
		offsetScaleX(0),
		offsetScaleY(0),
		offsetShearY(0),
		relative(0),
		local(0) {
	}
#endif
} spTransformConstraintData;

spPathConstraintData path路径约束

cpp 复制代码
typedef struct spPathConstraintData {
	const char* const name;
	int order;
	int bonesCount;
	spBoneData** const bones;
	spSlotData* target;
	spPositionMode positionMode;
	spSpacingMode spacingMode;
	spRotateMode rotateMode;
	float offsetRotation;
	float position, spacing, rotateMix, translateMix;

#ifdef __cplusplus
	spPathConstraintData() :
		name(0),
		bonesCount(0),
		bones(0),
		target(0),
		positionMode(SP_POSITION_MODE_FIXED),
		spacingMode(SP_SPACING_MODE_LENGTH),
		rotateMode(SP_ROTATE_MODE_TANGENT),
		offsetRotation(0),
		position(0),
		spacing(0),
		rotateMix(0),
		translateMix(0) {
	}
#endif
} spPathConstraintData;

总的说来,就是读取json,按照数据结构解析成spSkeletonData对象。

读取二进制

cpp 复制代码
void SkeletonRenderer::initWithBinaryFile (const std::string& skeletonDataFile, spAtlas* atlas, float scale) {
    _atlas = atlas;
    _attachmentLoader = SUPER(Cocos2dAttachmentLoader_create(_atlas));
    
    spSkeletonBinary* binary = spSkeletonBinary_createWithLoader(_attachmentLoader);
    binary->scale = scale;
    spSkeletonData* skeletonData = spSkeletonBinary_readSkeletonDataFile(binary, skeletonDataFile.c_str());
    CCASSERT(skeletonData, binary->error ? binary->error : "Error reading skeleton data file.");
    spSkeletonBinary_dispose(binary);
    
    setSkeletonData(skeletonData, true);
    
    initialize();
}

同样,解析成spSkeletonData 数据的在spSkeletonBinary_readSkeletonDataFile

cpp 复制代码
spSkeletonData* spSkeletonBinary_readSkeletonDataFile (spSkeletonBinary* self, const char* path) {
	int length;
	spSkeletonData* skeletonData;
	const char* binary = _spUtil_readFile(path, &length);
	if (length == 0 || !binary) {
		_spSkeletonBinary_setError(self, "Unable to read skeleton file: ", path);
		return 0;
	}
	skeletonData = spSkeletonBinary_readSkeletonData(self, (unsigned char*)binary, length);
	FREE(binary);
	return skeletonData;
}

spSkeletonBinary_readSkeletonData 函数按照二进制的数据结构读取,然后解析成spSkeletonData,二进制的数据结构在

https://zh.esotericsoftware.com/spine-binary-format#二进制导出文件格式

spSkeletonData数据结构和上面json的是一样的。

实例化数据对象

先看看官方给出的流程图

可以发现,解析完数据之后,就是instance Data,即实例化数据。

cpp 复制代码
void SkeletonRenderer::setSkeletonData (spSkeletonData *skeletonData, bool ownsSkeletonData) {
	_skeleton = spSkeleton_create(skeletonData);
	_ownsSkeletonData = ownsSkeletonData;
}

拿骨骼举例,数据解析为spBoneData ,那么实例化数据对象就是spBone

怎么理解呢?

Setup Pose Data,也就是解析Json或者Binary,对数据只是做了一次预处理,它并不会完全的去将所有数据处理成后面渲染的时候能用的数据。

两者有一定的区别

  1. 数据是无状态的,可在任意数量的骨架实例间共用
  2. 数据中的属性代表装配姿势,通常不会改动
  3. 实例对象中的相同属性表示播放动画时该实例的当前姿势。每个实例对象保有一个其数据参考,用于将实例对象重置回装配姿势。

可以理解成,数据是原始数据,一般不会涉及到对其进行转换修改等。实例数据对象就是将数据处理成后续渲染的时候需要用的数据,里面会有一些状态记录,坐标转换等方法。

这样做的一好处就是,备份原始数据,方便状态还原。

实例中有两个比较关键的方法,updateWorldTransformsetToSetUpPose

updateWorldTransform为更新世界变换,本质是触发骨骼位置的计算,由于骨骼位置可能发生旋转偏移,其对应的子骨骼也会受到影响,因此需要更新世界变换重新计算所有骨骼的最新坐标位置。

setToSetUpPose为更新实例到当前初始状态,一般才初始化时或重置人物状态时调用,会将人物形象骨骼装扮等切换为初始最初的状态。

Animation处理

实例对象生存之后,会调用Animation的处理

动画模块是被单独抽离出来的,目的是为了方便维护和更新实例的状态信息。

由动画state实例去触发skeleton实例的更新,接下来skeleton实例调用updateWorldTransform更新世界变化,之后重新上屏渲染。

cpp 复制代码
void SkeletonAnimation::initialize () {
	super::initialize();

	_ownsAnimationStateData = true;
	_state = spAnimationState_create(spAnimationStateData_create(_skeleton->data));
	_state->rendererObject = this;
	_state->listener = animationCallback;
}

在cocos2dx中,类为SkeletonAnimation ,继承于SkeletonRenderer

该函数会生存两个对象:

  • spAnimationStateData:存储AnimationState动画更改时要应用的混合(交叉淡入淡出)持续时间。
  • spAnimationState : 随着时间调用动画,动画入队等待播放,允许多个动画叠加。分多个track存储动画、区分不同动画的timeline,针对event事件的处理逻辑等。
相关推荐
QQ同步助手1 小时前
如何正确使用人工智能:开启智慧学习与创新之旅
人工智能·学习·百度
流浪的小新1 小时前
【AI】人工智能、LLM学习资源汇总
人工智能·学习
A懿轩A2 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
异次元的归来9 小时前
Unity DOTS中的share component
unity·游戏引擎
南宫生10 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
sanguine__10 小时前
Web APIs学习 (操作DOM BOM)
学习
向宇it12 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
数据的世界0112 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐12 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
向宇it13 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎