Pre-rendered 3D to 2D sprites: How many frames is too many?

Hi,

I’m rendering out a 3d idle animation of a character who is looping at 60 frames in a smooth animation with an eye blink at frame 20. This gives enough time in between the next blink to keep him from looking like he has an eye problem. Is rendering out to 59 images for use on a sprite sheet too many? I’ve usually heard anywhere between 4 and 10 frames is plenty, but in the case of a fully rendered 3d image, that’s too few frames.

Any advice would help. I’ve searched for this answer but I get sparse info and mostly irrelevant results. Thanks a lot.

How do you define “too many”? I’d define it as “performance drops because there’s too many texture sheets” or “there’s lack of memory to hold all those sprite sheets” and act accordingly (it doesn’t lag on target platform = it’s okay; it lags = needs decrease).

I’ve read many times in the past few decades that 12 frames is the minimum that looks nice for a walk animation. A blink should need less. Eye-blink animations you might want to do by attaching eyes with three frames of animation to the heads :slight_smile:

I think you’ll get the best result if you have two idle animations, one without the blink, and play the blink one randomly. 12-20 frames should be plenty if it’s semi-realistic art, but for a little side-project I had far less to work with and it still looked alright. Don’t aim for 60 frames in 2D art, as even Cuphead doesn’t do that.

1 Like

Thanks for the replies. I guess I’m just having trouble taking a semi-realistic CG character and making the idle animation not look like it’s missing frames like you’d see in pixel games. Because my character is on par with characters like Paranorman or Young Victor from Frankenweenie, the animation should look smooth all the way through, and I can’t get that in just 12 frames without making it look like it’s really looping. Maybe I just can’t explain any of this right.

I’d love to do the idle as two separate animations but I’m not sure how to play the eye blink randomly. Maybe some more advice or a good point in the right direction could help. Thanks a bunch guys.

I haven’t done it in plain Unity yet, but for 2D Toolkit I used a default idle animation, followed by an end event that picked the next idle animation to use. The different animations had a weight integer to decide how likely it was to appear. I’ll have to dig to see if I can find the algorithm and actually apply it to regular animations.

Thanks, Orb. I’m thinking that if I could do the idle animation in 12 frames, although it may seem fast, I could still slow it down in Unity, right? And then render out a secondary idle with the eye blink like you said and have it play randomly. That would be only 24 total sprites compared to 60, which could work. Is that something worth trying you think?

Yep, animation speeds can be altered. You don’t have to use 60, and probably never should for 2D. 3D is a whole different thing, where animation is just “a few” translations of vectors.

Found my old idle-state animation chooser thing. Now I have to figure out how the hell it works :stuck_out_tongue:

12 was nice but we moved up to 60 samples in the dopesheet. There isn’t a reason not to as you can space the keys apart, but it lets us have nice control over what frames are shown. The downside is that your app would need to run at 60fps to see them all if all are unique :wink:

Luckily for us it’s mostly just to allow some animations to execute faster - 12 was too slow.

I suppose I could retopologize the character and bring him into Unity as 3D to bypass all the sprite nonsense, but I’m not too comfortable with working in Unity with 3D characters. I suppose some fiddling is necessary here. Also, if I render out the animations as single PNG files, I could just import those into Unity, correct? I mean, wouldn’t it be nonsensical to put them on a single sprite sheet if I have to cut them up anyway? I’ve read that if you combine all like-images, such as characters, backgrounds and foregrounds, to individual atlases, you can save memory that way.

As you can tell, this entire process is still new to me. I’ve only done really small projects before. This one is my first real test.

Yeah, individual frames are fine. Unity generates new sheets, and you can group things the way you like by just tagging them correctly (Packing Tag on the sprites - select all the ones you want into one sheet and use the same name). One sheet per animation is also fine, if it’s easy to do. But a folder full of frames is nice and easy to inspect from anywhere inside or outside Unity.

Side note: Getting animation states from an animator seems to be a bit of a problem. The only method I’ve found involves using internal code, which puts it in risky territory. All I wanted to do was upgrade my custom idle state editor from 2DTK, but Mecanim still has fewer features :frowning:

Thanks for the tips. They really come in handy. As for your troubles, I hope you get them sorted out. Yours seems a bit more complex than mine unfortunately!

Another thing while we’re on the subject: I’ve read that you should use the biggest sprite images possible because you can scale them down if need be, but you can’t scale them up as they’d look pixelated. I’d like to output a 1080p game and would think 512x512 sprites would be large enough for the characters. Does anyone have any experience dealing with image sizes and whether going small or large would be a benefit? I guess it’s all about memory like Teravisor stated above.

[quote=“KrisBleenx, post:11, topic: 614768, username:KrisBleenx”]
As for your troubles, I hope you get them sorted out. Yours seems a bit more complex than mine unfortunately!
[/quote]Hah, it started so simple :slight_smile:

I’ve got a nice and simple solution based on one ScriptableObject that enumerates all animations and presents them in a dropdown to be added to an idle state controller (could be made more generic later, for randomised attack animations and so on). But the actual inspector is turning out to be an issue, as it’s not as simple as getting the animation library (like in 2DTK) and just taking those names.

As for your animations and choosing them, you should look at Mecanim triggers. Set a property in the Animator window, call SetTrigger() on the animation in the controller or add a transition from one state to another. At least I think it’s an either-or thing (still figuring out myself, as I’ve used 2DTK all the time).

Memory and download size, partially. But it’s a little more convoluted than that on mobile.

Any modern desktop, and even going as far back as five years ago, has support for 8192x8192 pixel textures. This is a massive boon to sprite sheet generation, but on mobile you’re still not guaranteed that even 2048x2048 is going to work on every device, and even if that’s more widespread now it’s still the safest maximum for Android.

If your sprites are 512x512 and aimed at Retina resolution on iOS you may be able to target 4096x4096 as your sprite sheet size (at least I think that’s the case with iPad Air and on, but things get weird with phones). If you’re supporting Retina devices with older GPUs, you can only fit 16 sprites in a sheet.

After experimenting with forced AA and different resolutions, it seems to me 256x256 can still be fine on high DPI displays. YMMV. Unity supports using different sizes for different devices too, so use as large source art as you can, and select the preferred maximum in the texture inspector’s dropdown :slight_smile:

Excellent advice; thanks again. Because I’m so noobish on mobile, I should probably aim at the desktop first, and then make the necessary changes to mobile. May spread myself too thin doing both at once since I’m not very knowledgeable when creating for touch-based devices.

It is a problem. But it’s solvable, kind of. I tried two ways: having a StateMachineBehaviour callback for exit state (but this proved problematic as I needed to then route all anims to an exit, which has it’s own issues) or using AnimatorStateInfo
normalizedTime <1 to check if playing.

My problem was simply enumerating the states from my custom editor, which I just figured out :slight_smile:
(Actual code for state-switching is yet to be written, but I’ve already done similar in 3D projects.)

That’s how I prefer it too. Make it as easily testable as possible for testers by making it desktop only at first. Think of how controls apply to touch along the way, and actually implement touch controls when you know you have something acceptable :slight_smile:

I’ll paste in the code I came up with, in case anybody wants something to build off.

IdleClip.cs:

using UnityEngine;

[System.Serializable]
public class IdleClip : ScriptableObject
{
    [SerializeField]
    public int weight;
    [SerializeField]
    public string clip;
    [SerializeField]
    public int index;

    public void OnEnable()
    {
        if(clip == null) clip = "";
    }
}

Just a class that keeps settings for each chosen idle-state. Put this somewhere and forget about it.

IdleController.cs:

using UnityEngine;
using System.Collections.Generic;

public class IdleController : MonoBehaviour
{
    [SerializeField]
    public string controllerPath;
    public List<IdleClip> clips;
    public int[] clipIndex;

    public string clip
    {
        get
        {
            if(clips.Count == 0) return "";

            int num = Random.Range(0, clips[clips.Count - 1].weight);
            foreach(IdleClip ic in clips)
            {
                if(num <= ic.weight) return ic.name;
            }
            return "";
        }
    }

    public static int CompareWeight(IdleClip a, IdleClip b)
    {
        if(a.weight == b.weight) return 0;
        if(a.weight < b.weight) return -1;
        return 1;
    }

    void Awake()
    {
        if(clips.Count > 0) clips.Sort(CompareWeight);
    }
}

Attach to a game object. Maybe not the best name, but IdleController returns the name of a random clip from the property “clip”.

The big one, IdleControllerEditor.cs:

using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;
using System.Collections.Generic;

[CustomEditor(typeof(IdleController))]
public class IdleControllerEditor : Editor
{
    public AnimatorController controller;
    private List<string> clips;
    private IdleController ic;

    void OnEnable()
    {
        ic = (IdleController)target;
        if(ic.controllerPath != null && ic.controllerPath != "")
        {
            controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(ic.controllerPath);
        }

        clips = new List<string>();
        if(controller == null)
        {
            return;
        }

        foreach(AnimationClip clip in controller.animationClips)
        {
            clips.Add(clip.name);
        }

        UpdateIndices();
    }

    void UpdateIndices()
    {
        if(clips.Count == 0) return;

        ic.clipIndex = new int[clips.Count];
        for(int i = 0; i < clips.Count; i++)
        {
            ic.clipIndex[i] = ic.clipIndex[i];
        }
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        if((ic != null) && (!Application.isPlaying))
        {
            EditorGUI.indentLevel = 0;

            controller = (AnimatorController)EditorGUILayout.ObjectField("Animation controller", controller, typeof(AnimatorController), false);
            if(controller != null)
            {
                ic.controllerPath = AssetDatabase.GetAssetPath(controller);
            }

            // Idle animations for non-carrying state
            GUILayout.Label("Idle animations");
            EditorGUILayout.Space();
            if(ic.clips.Count < clips.Count)
            {
                if(GUILayout.Button("Add"))
                {
                    IdleClip clip = ScriptableObject.CreateInstance<IdleClip>();
                    ic.clips.Add(clip);
                }
            }

            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.Space();
            GUILayout.Label("Clip");
            EditorGUILayout.Space();
            GUILayout.Label("Weight");
            EditorGUILayout.EndHorizontal();

            for(int i = 0; i < ic.clips.Count; i++)
            {
                if(ic.clips[i] == null) continue;

                EditorGUILayout.BeginHorizontal();
                EditorGUI.indentLevel = 1;
                ic.clips[i].index = EditorGUILayout.Popup(ic.clips[i].index, clips.ToArray());
                ic.clips[i].name = clips[ic.clips[i].index];
                EditorGUILayout.Space();
                if(ic.clips[i]) ic.clips[i].weight = EditorGUILayout.IntSlider(ic.clips[i].weight, 0, 100);
                if(GUILayout.Button("-")) ic.clips.Remove(ic.clips[i]);
                EditorGUI.indentLevel = 0;
                EditorGUILayout.EndHorizontal();
            }
        } else
        {
            EditorGUI.indentLevel = 0;
        }
        EditorUtility.SetDirty(target);
        serializedObject.ApplyModifiedProperties();
    }
}

Put in an Editor folder.

When inspecting an IdleController object this will show a custom inspector where you can drop a Mecanim animation controller and set which animations should play as idle animations, with weights to alter probability. It should only show an add button if you have added fewer clips than the controller has.

This is hardcoded to a 0-100 range at the moment. If you set two clips at 50 and 100, the lowest one will happen when the random number is 0-50, the other one at 51-100. Actually playing animations is left as an exercise to people with time on their hands :slight_smile:

Awesome makes a lot of sense, and great code work. Once I get out of the 3D program and into Unity, I’ll have to try this out. Thanks a bunch.