Spine深入学习 —— 数据

atlas数据的处理

作用

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

结构

page 页块

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

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%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-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结构:

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

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 __cplusplus
	spAttachmentLoader () :
					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 __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插槽信息

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皮肤

typedef struct spSkin {
	const char* const name;

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

spEventData事件数据

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时间线数据

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约束数据

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约束数据

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路径约束

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

读取二进制

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【点云surface】Poisson表面重建

1 介绍 Poisson表面重建算法是一种用于从点云数据生成平滑曲面模型的算法。它基于Michael Kazhdan等人在2006年发表的论文《Poisson surface reconstruction》。该算法通过将点云数据转换为体素表示&#xff0c;并利用Poisson方程来重建曲面。 该算法的基本原理是将点云数据转…

python教程:正常shell与反弹shell

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 正常shell需要先在攻击端开机情况下开启程序,然后攻击端运行程序,才能连接 反弹shell,攻击端是服务端,被攻击端是客户端 正常shell,攻击端是客户端,被攻击端是服务端 反弹shell,先启用服务端,再启用客户端 反弹shell的好处…

2022年09月 Scratch(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 运行下列程序后,结果为120的是? A: B: C: D: 答案:C 本题考察阶乘知识,12345的结果为120. <

【Python自学】七个超强学习网站,你值得拥有!

学习Python最主要的还是要动手&#xff0c;去找一些自己感兴趣的脚本&#xff0c;代码去练习&#xff0c;练的越多&#xff0c;对于一些英语单词&#xff0c;特殊符号要比死记硬背要容易记得些。 以下这些网站&#xff0c;虽说不上全方位的满足你的需求&#xff0c;但是大部分也…

基于springboot实现高校食堂移动预约点餐系统【项目源码】

基于springboot实现高校食堂移动预约点餐系统演示 Java语言简介 Java是由SUN公司推出&#xff0c;该公司于2010年被oracle公司收购。Java本是印度尼西亚的一个叫做爪洼岛的英文名称&#xff0c;也因此得来java是一杯正冒着热气咖啡的标识。Java语言在移动互联网的大背景下具备…

城市NOA加速落地,景联文科技高质量数据标注助力感知系统升级

当前&#xff0c;自动驾驶技术的演进正在经历着从基础L2到L3过渡的重要阶段&#xff0c;其中NOA&#xff08;自动辅助导航驾驶&#xff09;扮演着至关重要的角色。城市NOA&#xff08;L2.9&#xff09;作为城市场景下的NOA&#xff0c;被看作是车企向更高阶自动驾驶迈进的必经之…

汽车业务增长乏力!又被法雷奥告上法庭,英伟达有点「难」

随着智能汽车进入「降本增效」的关键周期&#xff0c;对于上游产业链&#xff0c;尤其是芯片的影响也在持续发酵。 本周&#xff0c;英伟达发布截至2023年10月29日的第三季度财报数据&#xff0c;整体业务收入为181.2亿美元&#xff0c;比去年同期增长206%&#xff0c;比上一季…

OSG粒子系统与阴影-爆炸模拟(3)

爆炸模拟示例 爆炸模拟示例的代码如程序清单11-4 所示&#xff1a; /* 爆炸模拟示例 */ void explosion_11_4() {osg::ref_ptr<osgViewer::Viewer> viewer new osgViewer::Viewer();osg::ref_ptr<osg::GraphicsContext::Traits> traits new osg::GraphicsContex…

基于袋獾算法优化概率神经网络PNN的分类预测 - 附代码

基于袋獾算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于袋獾算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于袋獾优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

C语言,通过数组实现循环队列

实现循环队列最难的地方就在于如何判空和判满&#xff0c;只要解决了这两点循环队列的设计就没有问题。接下来我们将会使用数组来实现循环队列。 接下来&#xff0c;为了模拟实现一个容量为4的循环队列&#xff0c;我们创建一个容量为4 1 的数组。 接下来我们将会对这个数组…

Kafka系列 - Kafka一篇入门

Kafka是一个分布式流式处理平台。很多分布式处理系统&#xff0c;例如Spark&#xff0c;Flink等都支持与Kafka集成。 Kafka使用场景 消息系统&#xff1a;Kafka实现了消息顺序性保证和回溯消费。存储系统&#xff1a;Kafka把消息持久化到磁盘&#xff0c;相比于其他基于内存的…

x86 汇编语言介绍001

1&#xff0c;搭建编程环境 1.1 NASM 基本信息 示例使用的汇编器为 nasm 主页&#xff1a; https://www.nasm.us/https://www.nasm.us/ 下载最新的稳定版源代码 wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/nasm-2.16.01.tar.gz 1.2解压并编译安装 tar zx…

89. 打家劫舍【动态规划】

题目 题解 class Solution:def rob(self, nums: List[int]) -> int:N len(nums)# 定义状态: dp[i]表示从第i间房子开始抢劫&#xff0c;最多能抢到的金额dp [0 for i in range(N)]for i in range(N-1, -1, -1):if i N-1:dp[i] nums[i]elif i N-2:dp[i] max(nums[i], …

案例-某验四代滑块反爬逆向研究一

系列文章目录 第一部分 案例-某验四代滑块反爬逆向研究一 文章目录 系列文章目录前言一、分析流程二、定位 w 值生成位置三、device_id 值的定位生成四、pow_msg 值 和 pow_sign 值的生成总结 前言 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff…

训练日志——wandb

目录 安装与登录基础使用与可视化常用函数wandb.init()wandb.config()wandb.log()wandb.finish()wandb.watch() 参考 安装与登录 安装 pip install wandb注册并登录 https://wandb.ai/site客户端登陆 在终端中输入wandb login 然后出现You can find you API key的一串网站&am…

Mysql数据库 20.DCL数据控制语言

因这类SQL语言开发人员操作较少&#xff0c;主要是数据库管理员&#xff08;DBA&#xff09;使用&#xff0c;所以前文没有提及&#xff0c;这篇文章进行补充说明 DCL数据控制语言 用来管理数据库用户&#xff0c;控制数据库的访问权限 1.管理用户 1.1 查询用户 select * f…

软件测试 | MySQL 非空约束详解

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

【c++哈夫曼树代码实现】

哈夫曼树是不定长编码方式&#xff0c;由于是将权值大的元素放在离根结点近的地方 &#xff0c;权值小的放在离根远的地方&#xff0c;哈夫曼树效率很高&#xff0c;并且一个编码不会以另一个编码作为前缀&#xff0c;避免了编码的歧义性&#xff0c;本文将带大家探索如何创建和…

【Apache Doris】一键实现万表MySQL整库同步 | 快速体验

【Apache Doris】一键实现万表MySQL整库同步 | 快速体验&#xff09; 一、 环境信息1.1 硬件信息1.2 软件信息 二、 流程介绍三、 前提概要3.1 安装部署3.2 JAR包准备3.2.1 数据源3.2.2 目标源 3.3 脚本模版 四、快速体验五、常见问题5.1 Mysql通信异常5.2 MySQL无Key同步异常5…

excel一个单元格换行方法

要是在同一个单元格内输入文字输入不下的话&#xff0c;我们是可以进行同一个单元格换行设置的&#xff0c;而且换行的方法也是有很多种&#xff0c;下面我们就一起来看一下有哪些方法吧。 excel一个单元格换行方法&#xff1a; 方法一&#xff1a; 1、我们可以直接按下alte…