Spine深入学习 —— 数据

atlas数据的处理

作用

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

结构

page 页块

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

page1.pngsize: 640, 480format: RGBA8888filter: Linear, Linearrepeat: nonepma: 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%20Getting%20started/06%20Textures/

  • 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)(10.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:

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

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

bg-dialogindex: -1rotate: falsebounds: 519, 223, 17, 38offsets: 2, 2, 21, 42split: 10, 10, 29, 10pad: -1, -1, 28, 10L1/L1_01rotate: truexy: 1855, 118size: 73, 78orig: 211, 216offset: 69, 69index: -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结构:

struct spAtlas {spAtlasPage* pages;spAtlasRegion* regions;void* rendererObject;
};

其中spAtlasPage如下:

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,其结构为

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

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

typedef enum {SP_ATLAS_MIRROREDREPEAT,SP_ATLAS_CLAMPTOEDGE,SP_ATLAS_REPEAT
} spAtlasWrap;

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

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

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

大概实现如下

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 >= 0x00031200ssize_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属性对应的图片的纹理。具体创建是

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

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的对象。

typedef struct spAtlasAttachmentLoader {spAttachmentLoader super;spAtlas* atlas;
} spAtlasAttachmentLoader;typedef struct spAttachmentLoader {const char* error1;const char* error2;const void* const vtable;
#ifdef __cplusplusspAttachmentLoader () :error1(0),error2(0),vtable(0) {}
#endif
} spAttachmentLoader;

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

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
"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状态的骨骼数据.

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

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

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

"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约束

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

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

"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约束

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

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

"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(皮肤)
"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"这个字样.

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

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

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

事件(Events)

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

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

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

"animations": {"name": {"bones": { ... },"slots": { ... },"ik": { ... },"deform": { ... },"events": { ... },"draworder": { ... },},...
}
骨骼时间轴

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

{
"bones": {"boneName": {"timelineType": [{ "time": 0, "angle": -26.55 },{ "time": 0.1333, "angle": -8.78 },...],...},...
},

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

在这里插入图片描述

槽位时间轴

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

"slots": {"slotName": {"timelineType": [{ "time": 0.2333, "name": "eyes closed" },{ "time": 0.6333, "name": "eyes open" },...],...},...
}

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

在这里插入图片描述

IK约束时间轴

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

"ik": {"constraintName": [{ "time": 0.5333, "mix": 0.616, "bendPositive": true },...],...
},

在这里插入图片描述

Path约束时间轴

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

"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"部分描述了用于操作每个网格附件的顶点(网格变形)的时间轴.

"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"部分描述了一个用于触发事件的时间轴.

"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上需绘制附件的槽位列表.

"draworder": [{"time": 0.2,"offsets": [{ "slot": "slotName", "offset": 1 },{ "slot": "slotName", "offset": -2 },...]},...
}

在这里插入图片描述

二进制数据

参考:https://zh.esotericsoftware.com/spine-binary-format#%E6%A0%BC%E5%BC%8F

读取Json

typedef struct spSkeletonJson {float scale;spAttachmentLoader* attachmentLoader;const char* const error;
} spSkeletonJson;

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

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对象,即二进制对象。

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骨骼的信息

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 __cplusplusspBoneData() :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插槽信息

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 __cplusplusspSlotData() :index(0),name(0),boneData(0),attachmentName(0),color(),darkColor(0),blendMode(SP_BLEND_MODE_NORMAL) {}
#endif
} spSlotData;

spSkin皮肤

typedef struct spSkin {const char* const name;#ifdef __cplusplusspSkin() :name(0) {}
#endif
} spSkin;

spEventData事件数据

typedef struct spEventData {const char* const name;int intValue;float floatValue;const char* stringValue;#ifdef __cplusplusspEventData() :name(0),intValue(0),floatValue(0),stringValue(0) {}
#endif
} spEventData;

spAnimation时间线数据

typedef struct spAnimation {const char* const name;float duration;int timelinesCount;spTimeline** timelines;#ifdef __cplusplusspAnimation() :name(0),duration(0),timelinesCount(0),timelines(0) {}
#endif
} spAnimation;

spIkConstraintData IK约束数据

typedef struct spIkConstraintData {const char* const name;int order;int bonesCount;spBoneData** bones;spBoneData* target;int bendDirection;float mix;#ifdef __cplusplusspIkConstraintData() :name(0),bonesCount(0),bones(0),target(0),bendDirection(0),mix(0) {}
#endif
} spIkConstraintData;

spTransformConstraintDataTransform约束数据

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 __cplusplusspTransformConstraintData() :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路径约束

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 __cplusplusspPathConstraintData() :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对象。

读取二进制

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

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#%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%AF%BC%E5%87%BA%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F

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

实例化数据对象

先看看官方给出的流程图

在这里插入图片描述

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

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更新世界变化,之后重新上屏渲染。

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事件的处理逻辑等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/214477.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flutter之Graphic图表的简单示例

简介 Graphic是一个数据可视化语法和Flutter图表库。 官方github示例 我的gitee示例 网上可用资源很少&#xff0c;只有作者的几篇文章&#xff0c;并且没有特别详细的文档&#xff0c;使用的话还是需要一定的时间去调研&#xff0c;在此简单记录。 示例 以折线图为例&…

GEE:生成超链接方式下载影像

作者:CSDN @ _养乐多_ 本文将介绍如何使用Google Earth Engine(GEE)平台以生成下载超链接的形式下载遥感数据。 结果如下图所示,只需点击链接,即可下载数据到本地。 文章目录 一、函数详解二、代码示例一、函数详解 用法返回值Image.getDownloadURL(params, callback)Ob…

土壤教学经典用图30张

一、土壤分布 二、土壤形成与气候 三、土壤形成与地形 四、土壤形成与成土母质 五、成土过程示意图 六、土壤剖面实景图 七、土壤剖面示意图 八、土壤质地 以上图片多来源于 人教、湘教、鲁教、中图、沪教 五套新教材及地图册

2023亚太杯数学建模C题高质量成品论文32页(附带所有小问配套代码)

数据收集&#xff08;C题成品获取在文末&#xff09; 全球汽车行业数据:收集关于全球传统能源汽车和NEEVs的销售、市场份额、生产等数据.经济指标考虑收集全球经济指标如GDP、石油价格等,这些可能影响汽车行业的发展.政策和法规:搜集有关国家和地区对新能源汽车的支持政策和对传…

YOLOv3老矣尚能战否?基于YOLOv3开发构建建钢铁产业产品智能自动化检测识别系统,我们来与YOLOv5进行全方位对比评测

钢铁产业产品智能自动化检测识别相关的项目在我们前面的博文中已经有了相应的实践了&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a;《python基于DETR(DEtection TRansformer)开发构建钢铁产业产品智能自动化检测识别系统》 《AI助力钢铁产业数字化&#xff0c;pytho…

每日OJ题_算法_双指针_力扣11. 盛最多水的容器

力扣11. 盛最多水的容器 11. 盛最多水的容器 - 力扣&#xff08;LeetCode&#xff09; 难度 中等 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成…

【云原生 Prometheus篇】Prometheus架构详解与核心组件的应用实例(Exporters、Grafana...)

Prometheus Part1 一、常用的监控系统1.1 简介1.2 Prometheus和zabbix的区别 二、Prometheus2.1 简介2.2 Prometheus的主要组件1&#xff09;Prometheus server2&#xff09;Exporters3&#xff09;Alertmanager4&#xff09;Pushgateway5&#xff09;Grafana 2.3 Prometheus的…

Anthropic推出Claude 2.1聊天机器人;使用AI工具写作:挑战与策略

&#x1f989; AI新闻 &#x1f680; Anthropic推出Claude 2.1聊天机器人&#xff0c;支持20万个Token输入和提高准确度 摘要&#xff1a;Anthropic推出了Claude 2.1聊天机器人及对应的AI模型&#xff0c;支持输入多达20万个Token&#xff0c;并在准确度上有所改善。Claude已…

mysql查询表的字段,字段名以及注释sql语句

sql语句如下&#xff1a; selecta.ordinal_position 序号,a.COLUMN_name 字段名,a.COLUMN_type 字段类型,(case a.is_nullable when NO then 是 else 否 end) 是否非空,(case a.column_key when PRI then 是 else 否 end) 是否主键,a.COLumn_comment 注释 frominformation_sch…

代码随想录算法训练营第四十五天【动态规划part07】 | 70. 爬楼梯 (进阶)、322. 零钱兑换、279.完全平方数

70. 爬楼梯 &#xff08;进阶&#xff09; 题目链接&#xff1a; 题目页面 求解思路&#xff1a; 动规五部曲 确定dp数组及其下标含义&#xff1a;爬到有i阶楼梯的楼顶&#xff0c;有dp[i]种方法递推公式&#xff1a;dp[i] dp[i-j];dp数组的初始化&#xff1a;dp[0] 1;确…

代码规范有用吗?听听100W年薪谷歌大佬怎么说!

谷歌内部的 python 代码规范 熟悉 python 一般都会努力遵循 pep8 规范&#xff0c;也会有一些公司制定内部的代码规范。大公司制定规范的目的不是说你一定要怎样去使用编程语言&#xff0c;而是让大家遵守同一套规则&#xff0c;节省其他人阅读代码的成本&#xff0c;方便协作…

好用的CRM系统到底有多重要?怎么选?

我们都知道&#xff0c;CRM软件可以让企业效率加倍。但如果选错了CRM&#xff0c;企业损失点钱是小&#xff0c;客户转化率下降才是大。下面我们就来说说&#xff0c;市面上有哪些好用的CRM&#xff1f;以及好用的CRM软件的重要性。 好用的CRM软件的重要性&#xff1a; 客户管…