manchangwei

目前在用Spine.Unity开发2D游戏,由于游戏的目标用户大部分是学生党,需要兼容大部分的低端机器,而且之前美术同学在Unity.Spine上没有使用经验,导致现在很多模型都是200根骨骼左右,所以想求助各位大神,有没有Spine的优化方案。
目前想到的方案是不同性能的机器使用不同的Spine模型,但是对于美术来说工作量较大,所以想到的方案是:
美术大大在Spine中编辑的时候,还是按照高端机型来做,但是会在一些骨骼上做特殊处理,比如在骨骼名字后缀加上_LQ,代表这根骨骼和它所有的孩子都可以在低端机上删除(这些骨骼主要用来做飘带,锁链等细节)。在加载Spine的时候,会检测带有这些后缀的骨骼名字,然后删除这些骨骼和骨骼相关的动画轨迹。我尝试了一下,直接在Spine编辑器中删除是没有任何问题的,但是在运行的时候,虽然成功删除了和这些LQ骨骼相关的动画轨迹,在Bone.UpdateWorldTransform中屏蔽了这些骨骼的更新,但是停止调用Bone.UpdateWorldTransform会导致在VertexAttachment.ComputeWorldVertices计算顶点数据的时候有错误,导致模型变形。各路大神有没有实现过类似的方案啊,指导一下小弟,感激不尽!
下面是我实现的大概思路:
public class SpineQuality
{
/// <summary>
/// 再次限制帧率
/// </summary>
public static float updateDelta = -1f;
/// <summary>
/// 运行的时候判断是否低端机,如果是,会将useLowerQuality设置true
/// </summary>
public static bool useLowerQuality = false;
/// <summary>
/// useLowerQuality为true的时候,加载Spine会过滤掉后缀到_LQ的骨骼和他们相关的Timeline
/// </summary>
private static string lowerQualitySuffix = "_LQ";
public static bool IsBoneLowerQuality(string boneName, BoneData parent)
{
if (boneName.EndsWith(lowerQualitySuffix))
{
return true;
}
if (parent != null)
{
return IsBoneLowerQuality (parent.name, parent.parent);
}
return false;
}

}
在SkeletonBinary中修改,判断哪些骨骼需要屏蔽

public SkeletonData ReadSkeletonData (Stream input) {
if (input == null) throw new ArgumentNullException("input");
float scale = Scale;

var skeletonData = new SkeletonData();
skeletonData.hash = ReadString(input);
if (skeletonData.hash.Length == 0) skeletonData.hash = null;
skeletonData.version = ReadString(input);
if (skeletonData.version.Length == 0) skeletonData.version = null;
skeletonData.width = ReadFloat(input);
skeletonData.height = ReadFloat(input);

bool nonessential = ReadBoolean(input);

if (nonessential) {
skeletonData.fps = ReadFloat(input);
skeletonData.imagesPath = ReadString(input);
if (skeletonData.imagesPath.Length == 0) skeletonData.imagesPath = null;
}
ReadBones (input, skeletonData, scale, nonessential);
// Slots.
ReadSlots (input, skeletonData);

// IK constraints.
ReadIKConstraints (input, skeletonData);

// Transform constraints.
ReadTransformConstraints(input, skeletonData, scale);
// Path constraints
ReadPathConstraints(input, skeletonData, scale);

// Default skin.
Skin defaultSkin = ReadSkin(input, skeletonData, "default", nonessential);
if (defaultSkin != null) {
skeletonData.defaultSkin = defaultSkin;
skeletonData.skins.Add(defaultSkin);
}

// Skins.
for (int i = 0, n = ReadVarint(input, true); i < n; i++)
skeletonData.skins.Add(ReadSkin(input, skeletonData, ReadString(input), nonessential));

// Linked meshes.
for (int i = 0, n = linkedMeshes.Count; i < n; i++) {
SkeletonJson.LinkedMesh linkedMesh = linkedMeshes[i];
Skin skin = linkedMesh.skin == null ? skeletonData.DefaultSkin :
skeletonData.FindSkin(linkedMesh.skin);
if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin);
Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent);
//if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent);
linkedMesh.mesh.ParentMesh = (MeshAttachment)parent;
linkedMesh.mesh.UpdateUVs();
}
linkedMeshes.Clear();

// Events.
for (int i = 0, n = ReadVarint(input, true); i < n; i++) {
EventData data = new EventData(ReadString(input));
data.Int = ReadVarint(input, false);
data.Float = ReadFloat(input);
data.String = ReadString(input);
skeletonData.events.Add(data);
}

// Animations.
for (int i = 0, n = ReadVarint(input, true); i < n; i++)
ReadAnimation(ReadString(input), input, skeletonData);

skeletonData.bones.TrimExcess();
skeletonData.slots.TrimExcess();
skeletonData.skins.TrimExcess();
skeletonData.events.TrimExcess();
skeletonData.animations.TrimExcess();
skeletonData.ikConstraints.TrimExcess();
skeletonData.pathConstraints.TrimExcess();
return skeletonData;
}

private void ReadBones(Stream input, SkeletonData skeletonData, float scale, bool nonessential)
{
// Bones.
for (int i = 0, n = ReadVarint(input, true); i < n; i++) {
String name = ReadString(input);
BoneData parent = i == 0 ? null : skeletonData.bones.Items[ReadVarint(input, true)];
bool isValid = true;
if (SpineQuality.useLowerQuality)
{
if (SpineQuality.IsBoneLowerQuality (name, parent))
{
isValid = false;
}
}
//
BoneData data = new BoneData(i, name, parent);
data.isValid = isValid;
data.rotation = ReadFloat(input);
data.x = ReadFloat(input) * scale;
data.y = ReadFloat(input) * scale;
data.scaleX = ReadFloat(input);
data.scaleY = ReadFloat(input);
data.shearX = ReadFloat(input);
data.shearY = ReadFloat(input);
data.length = ReadFloat(input) * scale;
data.transformMode = TransformModeValues[ReadVarint(input, true)];
if (nonessential) ReadInt(input); // Skip bone color.
skeletonData.bones.Add(data);
}
}
private void ReadAnimation (String name, Stream input, SkeletonData skeletonData)
{
var timelines = new ExposedList<Timeline>();
float scale = Scale;
float duration = 0;

// Slot timelines.
ReadSlotTimelines(input, skeletonData, timelines, ref duration);

// Bone timelines.
ReadBonesTimelines(input, skeletonData, timelines, scale, ref duration);

// IK timelines.
ReadIKsTimelines(input, skeletonData, timelines, ref duration);

// Transform constraint timelines.
ReadTransformConstraintsTimelines(input, skeletonData, timelines, ref duration);

// Path constraint timelines.
ReadPathConstraintTimelines(input, skeletonData, timelines,scale, ref duration);

// Deform timelines.
ReadDeformTimelines(input, skeletonData, timelines,scale, ref duration);
// Draw order timeline.
ReadDrawOrderTimelines(input, skeletonData, timelines, ref duration);

// Event timeline.
ReadEventTimelines(input, skeletonData, timelines, ref duration);

timelines.TrimExcess();
skeletonData.animations.Add(new Animation(name, timelines, duration));
}
void ReadBonesTimelines(Stream input, SkeletonData skeletonData, ExposedList<Timeline> timelines, float scale, ref float duration)
{
for (int i = 0, n = ReadVarint(input, true); i < n; i++)
{
int boneIndex = ReadVarint(input, true);
BoneData boneData = skeletonData.bones.Items [boneIndex];
for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++)
{
int timelineType = input.ReadByte();
int frameCount = ReadVarint(input, true);
switch (timelineType)
{
case BONE_ROTATE:
{
if (true)
{
RotateTimeline timeline = new RotateTimeline(frameCount);
timeline.boneIndex = boneIndex;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input));
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[(frameCount - 1) * RotateTimeline.ENTRIES]);
}
else
{

for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
ReadFloat (input);
ReadFloat (input);
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, null);
}
}
break;
}
case BONE_TRANSLATE:
case BONE_SCALE:
case BONE_SHEAR:
{
if (true) {
TranslateTimeline timeline;
float timelineScale = 1;
if (timelineType == BONE_SCALE)
timeline = new ScaleTimeline (frameCount);
else if (timelineType == BONE_SHEAR)
timeline = new ShearTimeline (frameCount);
else {
timeline = new TranslateTimeline (frameCount);
timelineScale = scale;
}
timeline.boneIndex = boneIndex;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
timeline.SetFrame (frameIndex, ReadFloat (input), ReadFloat (input) * timelineScale, ReadFloat (input)
* timelineScale);
if (frameIndex < frameCount - 1)
ReadCurve (input, frameIndex, timeline);
}
timelines.Add (timeline);
duration = Math.Max (duration, timeline.frames [(frameCount - 1) * TranslateTimeline.ENTRIES]);
}
else
{
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
ReadFloat (input);
ReadFloat(input);
ReadFloat (input);
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, null);
}
}
break;
}
}
}
}
}
然后在Skeleton中,创建骨架的时候,屏蔽掉LQ骨骼的更新
public Skeleton (SkeletonData data) {
if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
this.data = data;

bones = new ExposedList<Bone>(data.bones.Count);
foreach (BoneData boneData in data.bones) {
Bone bone;
if (boneData.parent == null)
{
bone = new Bone(boneData, this, null);
}
else
{
Bone parent = bones.Items[boneData.parent.index];
bone = new Bone(boneData, this, parent);
bone.isValid = boneData.isValid;
if (bone.isValid)
{
parent.children.Add(bone);
}
}
bones.Add(bone);
}

slots = new ExposedList<Slot>(data.slots.Count);
drawOrder = new ExposedList<Slot>(data.slots.Count);
foreach (SlotData slotData in data.slots) {
Bone bone = bones.Items[slotData.boneData.index];
Slot slot = new Slot(slotData, bone);
slots.Add(slot);
drawOrder.Add(slot);
}

ikConstraints = new ExposedList<IkConstraint>(data.ikConstraints.Count);
foreach (IkConstraintData ikConstraintData in data.ikConstraints)
ikConstraints.Add(new IkConstraint(ikConstraintData, this));

transformConstraints = new ExposedList<TransformConstraint>(data.transformConstraints.Count);
foreach (TransformConstraintData transformConstraintData in data.transformConstraints)
transformConstraints.Add(new TransformConstraint(transformConstraintData, this));

pathConstraints = new ExposedList<PathConstraint> (data.pathConstraints.Count);
foreach (PathConstraintData pathConstraintData in data.pathConstraints)
pathConstraints.Add(new PathConstraint(pathConstraintData, this));

UpdateCache();
UpdateWorldTransform();
}
在Bone的Update中直接屏蔽
public void Update () {
if (isValid == false)
return;
UpdateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
}
manchangwei
  • Mensajes: 2

Harald

我在这看到一个普遍的问题:
如果在某些低质量骨骼上设置“isValid == false”时阻止重新生成世界变换矩阵,则缺少将父变换应用于顶点。
I see a general problem here:
If you prevent the world-transformation matrix from being regenerated when you set isValid == false on some low-quality bones, then you are missing to apply the parent's transformations to the vertices.

请注意,UpdateWorldTransform的文档还指出:
Note that the documentation of UpdateWorldTransform also states:
Computes the world transform using the parent bone and the specified local transform.
您需要的是不更新本地变换,需要更新世界变换。因此,这不会为您节省大量的计算时间,因为您仍然需要应用低质量骨骼的初始设置姿势旋转。
What you would need is to not update the local transform, the world transform needs to be updated. As a consequence, this will not save you a lot of computation time, since you still need to apply the initial setup-pose rotation of the low-quality bone.

所以我们宁愿建议去除Spine中的骨头,这样会产生最好的效果。如果你真的需要以编程方式删除部件,你可以删除动画时间轴 - 但是这不会为你节省很多,因为骨头仍在那里并且需要更新世界变换,如上所述。
So we would rather suggest to either remove the bones in Spine which will have the best effect. If you really need to remove parts programmatically, you could maybe remove animation timelines - however this will not save you a whole lot, as the bones are still there and world-transforms need to be updated, as described above.
Avatar de Usuario
Harald

Harri
  • Mensajes: 850

manchangwei

谢谢,因为我们美术比较懒,想要程序动态删除,目前删除了时间轴后效率已经差不多了,如果后面在低端机还有效率问题,就只能让美术在Spine里面删除导出来解决。谢谢啦
manchangwei
  • Mensajes: 2

Harald

别客气。很高兴你能解决这个问题。
You're welcome. Glad you could resolve the problem.
Avatar de Usuario
Harald

Harri
  • Mensajes: 850


Volver a 中国Spine用户