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:

  1. Average Spine atlas size (2030x1297 px) - but, be honest, doesn't seems to be big nowadays
  2. 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);
    }
}

It's probably because there are too many viewports; one for each SpineSprite.

    You might think the isssue is with animation itself. So we did tests with sample Spine animation. Just one Spine animation in simple 2D takes almost 100% GPU.

    The same computer running Dota

    Something is not right with Godot version supporting Spine

    SilverStraw We have tried different approach. We did test with 5 active animations and 5 viewports - put on 100 meshes recycling 100 times the same animations. We can achieve more than 100 FPS in that way. Still, power of GPU needed for 5 skeleton animations is ridiculous.
    Test is done on different PC now.

    Worth to mention, we use 3D hack mentioned on that forum (Mesh3D + Texture from SubViewport)

    This is the problem. This is a very bad way to render anything. I'll eventually get to adding a SpineSprite3D. I'm afraid other things currently have priority.

    #1
    Please, please do it. Should be extremely simple to do that (I mean replace Node2D with Node3D)
    #2
    That is not explaining why pure Spine sample on 2D scene takes 100% GPU of GeForce 1060

      The reason of 100% GPU usage is unlocked FPS amount.

      It would be nice from Esoteric side to allow unlocking FPS without killing performance.

      BorysBe Please, please do it. Should be extremely simple to do that (I mean replace Node2D with Node3D)

      If it was as simple as that, we'd already have done it.

      That is not explaining why pure Spine sample on 2D scene takes 100% GPU of GeForce 1060

      If you disable vsync, then yes, your CPU and possibly GPU usage will go to 100%. That is not a Spine related issue, that's simply what disabling vsync will do to any app, be it Godot based or a simple OpenGL/DirectX/Metal/Vulkan app that just clears the screen.

        Mario I have some concerns because I do not know how Spine in Godot works, not because I wanted to be impolite. At least it was not my intention earlier.

        Try to understand what is happening in background

        1. what kind of calculation is executed by Spine library every frame? Is it possible that we do some redundant calculations which results is the same by 10 frames in a row and we could cache last result?
        2. Is Spine library using Godot objects like Node2D? Flat mesh in 3D space with Node3D instead of Node2D would have only Z-coordinate constant. There is no need to do any additional calculation. I don't understand what makes things so hard. Not saying that it is easy to do. Just saying don't see good reason to things not being simply.

          BorysBe lol go ahead and add this feature. @Mario very excited for 3D spines. Is there a way to contribute / fund the godot runtime beyond buying Spine?

            Can't you fork the runtime repo and add whatever?

            @TheSunOfMan sure, we do accept pull requests on the spine-runtimes repo, provided we get a signed contributor license agreement. Sorry for the red tape, but it allows us to publish contributions under our license. We've had quite a few great PRs over the years, including the initial spine-godot runtime (which was then heavily rewritten).

            @BorysBe

            1. Every frame, the bones will be posed, then attachment vertices will be calculated based on the bone poses. That's the same for either 2D or 3D. Have a look at Skeleton::updateWorldTransform(), Bone::update() etc. It's all extremely light weight calculations and usually not the case for any performance issues, especially given that spine-godot is based on spine-cpp, our C++ runtime.

            2. The Spine runtime directly talks to VisualServer and RenderServer for best performance. Adding (or removing) a z-coordinate per vertex will make absolutely not difference in terms of performance.

            As I said earlier, the performance issue you are seeing is due to your usage of viewports. spine-godot itself is very fast, both on the CPU and GPU side.

            As for simply switching between Node2D and Node3D, again, it's not that simple. Godot's internal rendering architecture distinguishes between 2D (Canvas) and 3D rendering (spatials). We need an entirely new rendering backend for 3D support. There is also the issue of transform coordinate spaces for methods that give you skeleton -> world transforms, or vice versa. Finally, both SpineSprite and SpineMesh2D are subclasses of Node2D, which does not have the same interface as Node3D. We can't thus just swap out the base class and call it a day. For supporting both 2D and 3D, SpineSprite's functionality needs to be taken apart, putting common pieces into a base class, then have separate SpineSprite and SpineSprite3D classes. All while keeping compatibility with existing projects.

            As I said, if it was easy, we'd already have done it. It's not. It's a lot of work, for which we currently don't have the cycles.

            That is not explaining why pure Spine sample on 2D scene takes 100% GPU of GeForce 1060

            A quick note: when rendering in any game toolkit / software is not limited by vsync or something else then it runs as fast as possible. If I modify the Spine editor to do this, it renders at over 3600 frames per second. With a simpler application I see over 8200 FPS. Rendering so fast naturally uses a lot CPU and GPU.

            2 meses más tarde

            Is there any workaround? Or recommended way to use spine animations in 3d scenes?

            There is no workaround until we implement a 3D SpineSprite equivalent.