运行时骨架(Skeleton)
Spine运行时提供了对骨骼、槽位、附件等等的访问. Skeleton可被定制并以多种方式对环境做出响应.
世界变换
骨骼(bones)以层级方式组织, 每根骨骼均受其父骨骼影响, 影响可一直追溯到根骨骼. 例如, 当一根骨骼旋转时, 所有子骨骼也会随之旋转. 每根骨骼都包含一个本地变换来实现这点, 其中有:
x
和y
rotation
scaleX
和scaleY
shearX
和shearY
运行时会从根骨骼开始以骨骼的父子顺序来处理变换, 即使用本地变换来计算每根骨骼的世界变换. 世界变换中包含着:
a
、b
、c
及d
这是一个2x2矩阵, 内含了骨骼及其所有父骨骼的旋转、缩放和剪切.a
和c
作用于X轴,b
和d
则是Y轴.worldX
和worldY
是骨骼的世界位置, 根骨骼采用的坐标系便是世界坐标系.
世界变换可将骨骼的本地坐标变换为世界坐标. 例如可将附加于某根骨骼的网格顶点(本地坐标)变换为骨骼的世界坐标. 变换后的顶点坐标受该骨骼及其全部父骨骼影响. 因此这一机制是Spine Skeleton动画系统的核心机制.
还可以反向变换世界坐标, 即可以将任一点世界坐标转换为该骨骼的本地坐标.
updateWorldTransform
我们通常不必直接修改骨骼的世界变换, 而是去更改本地变换, 再使用本地变换和父骨骼的世界变换来计算出世界变换. 每次修改了骨骼的本地变换后, 都必须调用 Bone 的 updateWorldTransform来重新计算该骨骼及其所有子骨骼的世界变换. 此外, 骨骼还必须按正确顺序更新, 所以更常见的方法是调用Skeleton 的 updateWorldTransform
方法, 它不仅能按正确顺序更新所有骨骼的世界变换, 还同时会应用所有的约束.
应用动画几乎总会修改骨骼的本地变换, 但渲染Skeleton却用的是骨骼的世界变换. 所以在应用动画后到开始渲染前的这段时间里, 常会调用 updateWorldTransform
.
state.apply(skeleton);
skeleton.updateWorldTransform();
renderSkeleton(skeleton);
程序化(Procedural)动画
可使用程序化的方式来访问和调整骨骼, 以此实现各种效果. 例如, 你可以设置骨骼的本地旋转使其指向鼠标指针来进行瞄准. 以程序化方式来定位IK目标骨骼也很方便, 可使IK约束以各种IK mixes来来调整各种骨骼.
通常的做法是在应用动画后再调整骨骼:
...
state.update(delta);
state.apply(skeleton);
torso.rotation = ... // compute rotation for torso
skeleton.updateWorldTransform();
renderSkeleton(skeleton);
如果需要根据动画姿势的世界变换来调整骨骼, 可在调整前调用updateWorldTransform
, 在本地变换更改后再次调用 updateWorldTransform
:
...
state.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform();
torso.rotation = ... // compute rotation for torso
skeleton.updateWorldTransform();
renderSkeleton(skeleton);
Setup pose(初始姿势)
运行时是基于Setup pose来应用动画的, 这意味着如果调整了 BoneData, 则这一更改会影响使用该BoneData的所有Skeleton 动画. 应用动画会使用BoneData, 因此应在运行时应用动画前完成更改:
...
torso.data.rotation = ... // compute rotation for torso
state.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform();
renderSkeleton(skeleton);
骨骼位置
可使用骨骼的世界变换设置游戏元素的位置, 例如粒子或其他效果:
...
state.update(delta);
state.apply(skeleton);
skeleton.updateWorldTransform();
renderSkeleton(skeleton);
renderParticles(rightHand.worldX, rightHand.worldY);
此例中应用了一段动画, 计算世界变换后开始渲染Skeleton, 然后在骨骼"right hand"的世界位置上绘制粒子效果. 也可以用该骨骼的世界旋转和缩放, 例如, 沿着骨骼方向发射粒子. 也可用同样的方法来使用骨骼的世界变换, 通过移动、旋转及缩放UI元素来实现UI动画.
通用渲染
游戏工具特化的运行时均为完整的解决方案, 它们包括渲染在内的所有功能. 通用运行时则与游戏工具无关, 除了实际渲染外什么都做. 因此使用通用运行时的时候才需要考虑如何渲染.
要在通用运行时上进行渲染, 可使用Skeleton类的drawOrder属性, 这是一个按绘制顺序排列的槽位列表. 渲染需要从各槽位中获取附件, 检测其类型并适时按需渲染. 需要渲染的附件类型包括:
RegionAttachment
是有4个顶点的四边形(它不总是矩形).MeshAttachment
包含任意数量的顶点和三角形. 顶点和三角形均由skeleton数据提供, 渲染器无需自己着手实现三角化.
渲染过程的伪代码如下所示:
Attachment attachment = slot.attachment;
AtlasRegion region;
if (attachment is RegionAttachment) {
attachment.computeWorldVertices(slot.bone, vertices);
triangles = quadTriangles;
region = attachment.rendererObject;
} else if (attachment is MeshAttachment) {
attachment.computeWorldVertices(slot.bone, vertices);
triangles = attachment.triangles;
region = attachment.rendererObject;
}
if (texture != null) {
Texture texture = region.page.rendererObject;
draw(texture, vertices, triangles, slot.data.blendMode);
}
}
运行时加载Skeleton数据时, AttachmentLoader将会设置附件的rendererObject
属性. 此例中假定使用了Spine atlas, 所以其中的rendererObject
是一个 AtlasRegion.
AtlasRegion包含一个AtlasPage, 它有自己的rendererObject
属性, 在加载atlas时TextureLoader会设置这一属性. Texture类则代表某个具体游戏工具版本的纹理类.
更换附件
在任意时刻中, 槽位可包含一个附件也可不含附件, 可通过调用Slot的 attachment来更换槽位中的附件. Skeleton还有一个setAttachment方法, 可以方便地按名称查找槽位及其中的附件对象. 运行时将保留槽位中的附件直至它被更换.
// Find the slot by name.
Slot slot = skeleton.findSlot("slotName");
// Get the attachment by name from the skeleton's skin or default skin.
Attachment attachment = skeleton.getAttachment(slot.index, "attachmentName");
// Sets the slot's attachment.
slot.attachment = attachment;
// Alternatively, the skeleton setAttachment method does the above.
skeleton.setAttachment("slotName", "attachmentName");
更换附件还有其他方式. 调用Skeleton的setToSetupPose
或setSlotsToSetupPose
也可以更换槽位附件. 动画的关键帧也能更换附件. 调用Skeleton的setSkin
同样能更换附件(详见更换皮肤).