Unity: Animated texture from image sequence – Part 2
Posted by Dimitri | Apr 25th, 2012 | Filed under Featured, Programming
The second and last tutorial of a series that explains how to create an animated texture from a sequence of images in the Unity engine. This is a direct follow up of the previous post, so if you’ve missed it, please read it here. As the first post, the same disclaimer applies: for animating a texture with a small number of frames and/or small frame sizes, it’s better to join them into a single sprite sheet image and use this script. For video playback, use the MovieTexture (only available at Unity Pro).
Just as a reminder, this series is about creating an animated texture from multiple image files. In the previous post, a script loaded all image files into an array and then changed the texture of the game object it was attached to. By doing so, a lot of memory was being used in the process. This post shows how to achieve the same results using much less memory but more processing power. To create animated texture in Unity like the one below:
It’s necessary to do the same as the previous post: create a folder named ‘Resources’ by right clicking anywhere inside the Project tab and selecting Create->Folder. Then, create a folder inside it with the name ‘Sequence’ and place all image files inside it, like this:
This time around, since the script doesn’t load all the images at the same time from the ‘Sequence’ folder, the following information must be written down to be used later on the script:
- The name of the subfolder created inside the ‘Resources’ folder, in this case ‘Sequence’.
- The base name of all image files. As it can be seen at the previous image taken from the example project, the base name is ‘Clock_’.
- The number of image files in the folder, which is 50.
- The number of digits added to the animation sequence name, which is 5.
Except for the last information, all the other ones are going to be used to fill the required fields on the Hierarchy tab when the script gets attached to a game object. Therefore, the code that loads an image sequence as an animated texture using just a single Texture object is going to be this:
using UnityEngine; using System.Collections; public class ImageSequenceSingleTexture : MonoBehaviour { //A texture object that will output the animation private Texture texture; //With this Material object, a reference to the game object Material can be stored private Material goMaterial; //An integer to advance frames private int frameCounter = 0; //A string that holds the name of the folder which contains the image sequence public string folderName; //The name of the image sequence public string imageSequenceName; //The number of frames the animation has public int numberOfFrames; //The base name of the files of the sequence private string baseName; void Awake() { //Get a reference to the Material of the game object this script is attached to this.goMaterial = this.renderer.material; //With the folder name and the sequence name, get the full path of the images (without the numbers) this.baseName = this.folderName + "/" + this.imageSequenceName; } void Start () { //set the initial frame as the first texture. Load it from the first image on the folder texture = (Texture)Resources.Load(baseName + "00000", typeof(Texture)); } void Update () { //Start the 'PlayLoop' method as a coroutine with a 0.04 delay StartCoroutine("PlayLoop", 0.04f); //Set the material's texture to the current value of the frameCounter variable goMaterial.mainTexture = this.texture; } //The following methods return a IEnumerator so they can be yielded: //A method to play the animation in a loop IEnumerator PlayLoop(float delay) { //wait for the time defined at the delay parameter yield return new WaitForSeconds(delay); //advance one frame frameCounter = (++frameCounter)%numberOfFrames; //load the current frame this.texture = (Texture)Resources.Load(baseName + frameCounter.ToString("D5"), typeof(Texture)); //Stop this coroutine StopCoroutine("PlayLoop"); } //A method to play the animation just once IEnumerator Play(float delay) { //wait for the time defined at the delay parameter yield return new WaitForSeconds(delay); //if it isn't the last frame if(frameCounter < numberOfFrames-1) { //Advance one frame ++frameCounter; //load the current frame this.texture = (Texture)Resources.Load(baseName + frameCounter.ToString("D5"), typeof(Texture)); } //Stop this coroutine StopCoroutine("Play"); } }
At the beginning of the above script, seven member variables are being declared. All the ones declared as public are self explanatory and are required to be initialized by filling their respective fields (in the Hierarchy tab) when the script is attached to a game object. These variables are the ones that are going to be filled with the information that was written down. They are all strings. (lines 14, 16 and 18).
The private variables consist of a Texture, a Material, an integer and a string. The Texture object is the one that will output the animation; the Material will act as a handle to the game object’s material the script is attached to and the integer is a counter that gets incremented to advance the animation frames (lines 7 through 11 and line 21).
At the Awake() method, the Material object is being initialized (lines 23 through 29), and, as previously mentioned, will act as a handle to the current game object Material. Also, this is where the baseName string is initialized. It holds the result of the concatenation of two strings: folderName and ImageSequenceName. These two must be passed at the Inspector tab when the script is attached to a game object. For the example project the resulting string that baseName will hold is “Sequence/Clock_“.
Next, the Start() method is being declared and it takes the first file of the ‘Sequence’ folder and sets it as the texture of the game object (lines 31 through 35). Then, the Update() method does exactly the same as the one presented at the previous post: the code execution is yielded the amount of time passed at the delay parameter and changes the current texture with the corresponding frame after the delay. The delay is also achieved through the use of IEnumerators (lines 37 through 43).
The PlayLoop() IEnumerator method is the one that causes the frames to change after the amount of seconds defined by the delay parameter. The code that gets yielded increments frameCounter and stores the rest of it’s division by numberOfFrames inside itself. Line 56 is the one that makes this method different from the one presented at the previous post. It loads the right image from from the ‘Sequence’ folder based on the frameCounter value. Note that the frameCounter value won’t contain trailing zeros when converted to a string. Since all the names of the images inside the sequence folder have at least 5 numbers, when frameCounter is converted to a string the “D5” parameter is passed, adding the correct number of zeros. The last line inside this method cancels any coroutine started at the Update() method which gets the texture animated (lines 47 through 60).
Lastly, the Play() method is being defined and works the same way as PlayLoop() except it only increments frameCounter if it’s value is smaller than the number of frames minus one. It also loads the correct image based on frameCounter (lines 63 through 80).
For this example, after attaching the script to a game object the ImageSequenceSingleTexture script parameters are going to be filled with the information we have wrote down before:
Final Thoughts
This script will somehow impact performance, because the animation frames are being loaded when they are required, which might cause some lag between the frames. Since it only uses a single Texture object for the animation, there is a lot less use of memory compared to the script presented on the first part of the series. However, it’s computationally heavy to load resources like this at execution time, and there’s a great chance the animation won’t play smoothly. So, it’s probably best to use this script when the animation contains o a lot of frames, but each frame is a small image (in pixels).
Therefore, something has got to give, either the memory or the processing — pick your poison. As stated on the beginning of the two posts, this should never be used for video playback. For that, use the MovieTexture component.
As promised, here’s the example project:
Why does my game show empty plane object before i start the scene with animation on it? It looks like a bug before my animation starts….
Fixed that..thanks anyway..but i would like to know how to change frame rate of video please… :)
To change the animation frame rate, just pass a different delay to the Play() and PlayLoop() methods when calling them as coroutines.
Thanks :) Worked!
Thanks for both of the tutorials :)
Hi
sorry i am a newbe, can you say me how to load and connect your Script to a Sphere with the animated texture ?
thanks in advance
Elay