Hello, I am writing to get some help from you Dear Spine masters π
Spine animations are being planned as a center of our Godot indie game but only few active Spine animations on the scren initialized manually (not with the Scene loading) ends immediately with significant (and I mean SIGNIFICANT) frame drops...
Before activating Spine animation it was 120 FPS
4 FPS on GF GTX 1050Ti after executing Spine animations
Worth to mention, we use 3D hack mentioned on that forum (Mesh3D + Texture from SubViewport) which bypass Spine-library limitation in 3D games and C# scripts, and - what is very important - we are initializing Spine animation on-the fly, not when Scene loading. Although performance seems to be much better when Spine animations initialize with scene, but - unfortunately - we cannot do that in that way.
At the beginning we thought the issue is with 2 things:
- Average Spine atlas size (2030x1297 px) - but, be honest, doesn't seems to be big nowadays
- Amount of Spine animations on the screen - 348 animated trees
Ok. At this point you may think - those guys are crazy to have 348 active animations on the screen.
Agree...
First we were trying to scale down Atlas image size twice - no satisfying effect.
Then, we limited amount of visible and active nodes with Spine animation
We did a lot of optimization (hide Spine animations arround your character)
35 Spine animations on the screen - 8 FPS
So we limited even more...
3 Spine animation - 27 FPS
Enough. Should be at least 60FPS! We need help!
I am attaching raw code which is doing initialization of Spine animation (nothing sophisticated but should show the problem)
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using Vector2 = Godot.Vector2;
using Vector3 = Godot.Vector3;
public partial class OnFlyLoadedSpineSprite : Node3D
{
public SpineSkeletonDataResource SpineSkeletonDataResource { get; set; } = new SpineSkeletonDataResource();
[Export]
public SpineAtlasResource AtlasResource
{
get => SpineSkeletonDataResource.AtlasRes;
set
{
SpineAtlasResource spineAtlasResource = value;
SpineSkeletonDataResource.AtlasRes = spineAtlasResource;
}
}
[Export]
public SpineSkeletonFileResource SkeletonFileResource
{
get => SpineSkeletonDataResource.SkeletonFileRes;
set
{
SpineSkeletonFileResource spineSkeletonFileResource = value;
SpineSkeletonDataResource.SkeletonFileRes = spineSkeletonFileResource;
}
}
[Export] bool Mirrored { get; set; }
[Export] public string NameOfAnimationToPlay { get; set; } = "idle";
[Export(PropertyHint.Range, "0,100,")] public int DeviationPercent = 0;
[Export] public float SortingOffset { get; set; } = 50.0f;
public const float PixelToMeterConversionRatio = 0.01f;
private const int DefaultTrackId = 0;
public MeshInstance3D MeshInstance3D { get; set; }
public SubViewport SubViewport { get; set; }
public SpineSprite SpineAnimationSprite { get; set; }
public void InitializeSpineAnimation()
{
SpineAnimationSprite = new SpineSprite()
{
SkeletonDataRes = SpineSkeletonDataResource,
UpdateMode = SpineConstant.UpdateMode.Process
};
if (Mirrored)
{
SpineAnimationSprite.Scale = new(-1 * SpineAnimationSprite.Scale.X, 1 * SpineAnimationSprite.Scale.Y);
}
var state = SpineAnimationSprite.GetAnimationState();
state?.SetAnimation(NameOfAnimationToPlay, true, DefaultTrackId);
var subViewportSize = CalculateSubViewportSize();
var (meshSizeWithoutDeviation, meshSize) = CalculateMeshSize();
var subviewport = new SubViewport()
{
TransparentBg = true,
Size = subViewportSize
};
SubViewport = subviewport;
SpineAnimationSprite.Position = new Vector2(subViewportSize.X / 2.0f, subViewportSize.Y);
MeshInstance3D = new MeshInstance3D()
{
Mesh = new QuadMesh()
{
Size = meshSize,
CenterOffset = new Vector3(meshSizeWithoutDeviation.X / 2.0f, meshSize.Y / 2.0f, 0)
},
Scale = Vector3.One,
SortingOffset = SortingOffset
};
var material = new StandardMaterial3D()
{
Transparency = BaseMaterial3D.TransparencyEnum.Alpha,
AlphaScissorThreshold = 0f,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
};
Texture2D texture = SubViewport.GetTexture();
material.AlbedoTexture = texture;
MeshInstance3D.MaterialOverride = material;
this.AddChild(MeshInstance3D);
MeshInstance3D.Owner = this;
MeshInstance3D.AddChild(SubViewport);
SubViewport.Owner = MeshInstance3D;
SubViewport.AddChild(SpineAnimationSprite);
SpineAnimationSprite.Owner = SubViewport;
SpineAnimationSprite.Visible = true;
MeshInstance3D.Visible = true;
}
private Vector2I CalculateSubViewportSize()
{
var skeletonSize = SpineAnimationSprite.GetSkeleton().GetBounds().Size;
var subViewportSize = new Vector2I((int)skeletonSize.X + 1, (int)skeletonSize.Y + 1);
if (DeviationPercent > 0)
{
var deviationX = subViewportSize.X * (DeviationPercent / 100.0f);
var deviationY = subViewportSize.Y * (DeviationPercent / 100.0f);
var deviation = new Vector2I((int)deviationX + 1, (int)deviationY + 1);
subViewportSize = subViewportSize + deviation;
}
return subViewportSize;
}
private List<Node> spritesDisplayedBeforeAnimationStarted = new List<Node>();
private Tuple<Vector2, Vector2> CalculateMeshSize()
{
this.spritesDisplayedBeforeAnimationStarted = this.GetChildren().Where(x => x is Sprite3D).ToList();
Vector2 meshSize;
if (spritesDisplayedBeforeAnimationStarted == null || spritesDisplayedBeforeAnimationStarted.Count == 0)
{
meshSize = SpineAnimationSprite.GetSkeleton().GetBounds().Size;
}
else
{
var firstSprite = (Sprite3D)spritesDisplayedBeforeAnimationStarted.FirstOrDefault(x => x is Sprite3D);
meshSize = firstSprite.Texture.GetSize();
}
meshSize *= PixelToMeterConversionRatio;
var meshSizeWithoutDeviation = meshSize;
if (DeviationPercent > 0)
{
var deviation = meshSize * (DeviationPercent / 100.0f);
meshSize += deviation;
}
return new Tuple<Vector2, Vector2>(meshSizeWithoutDeviation, meshSize);
}
}