This tutorial will show you how to generate and integrate 2D sprite sheets into your project. It covers 2 demo classes which are available to download from the examples on our github repository.
Contents
THE CLASSES REQUIRED FOR THIS TUTORIAL ARE AVAILABLE IN THE DEV BRANCHES OF EXAMPLES AND ENGINE
Introduction
Since the early 70’s sprite sheets have been used extensively in computer graphics to improve rendering performance and reduce memory consumption amongst other things. A sprite sheet is simply a single two-dimensional image containing one or more animation frames with each animation frame being rendered in a rectangular area on the image. The original animation can be faithfully reproduced by making one frame visible, masking the others and offsetting the viewport at the same speed of the original rendered animation. This is pretty much the same concept as used in the early animation systems from the late 1800’s such as the zeotrope in the picture above.
Storing animation information this way has many advantages:
- Memory savings
This is totally true in 2d projects, it allows you at a low memory consumption to play complex looking animations on low end hardware.
- Can be integrated into a larger scene.
You can transform the animation’s viewport into your project. 2 or 3D.
- Illusion
The animation can be very detailed and may replace either real world images or otherwise too expensive techniques. Our eyes can be easily fooled when a series of images with many similarities are played at a certain speed.
There are limitations too:
- Playback rate
In order to recreate the illusion of the original animation, it is important that the playback rate is as close to the original rate that was used during the generation of the spritesheet itself.
- The amount of frames that you can store
Even if you can store the information on multiple sprite sheets you cannot store as many frames as you would, for instance, with a video. Therefore this technique is only really suitable for short animations.
- It’s 2D
Since the early days of gaming, sprite sheets were used especially on 2d or 3d isometric projects because the possible 3d transforms rendered in the animation frames and are not altered by the transforms of the project viewport.
However in world of 3D, things are different. If you were to transform an already transformed animation frame, the camera point of view is in effect altered. Subsequently your eyes and brain will no longer accept the trick that easy. That’s why in 3D, spritesheets are usually used for billboarding or mapped onto a 3d shape to mimic a 2D object in the real world such as a TV screen, blinking lights etc…
The sprite sheets in Away3D
In order to use sprite sheets in your project, you will need to learn how to use the SpriteSheetAnimator
class, it’s helper class and a dedicated material. The animator takes care of the animation for you and uses the GPU for maximum efficiency and performance. You can import two kinds of data: spritesheet maps already defined, or let the helper class generate them for you runtime from movieclips stored in external swf files.
How to generate a spritesheet?
There are many software applications that generate sprite sheets for you, such as TexturePacker, Zoe, SpriteGenerator, Flash CS6 etc… If you use Flash CS6, there is a generator built into the IDE but you can do the same thing using an older flash version. Away3D provides a generator that builds sprite sheets from movieclips (see intermediate example bellow). Note that the automated generation works only with fixed frames sizes. Nested animations from other movieclips within the same map(s) are not supported. If you want to have nested animations of different kinds or have different frame sizes to maximize the use of the maps surface used, look at the advanced part of this tutorial to see how it can be done.
The materials
There are two possible materials that can be used for the spritesheet animations. If your spritesheet animation fits into a single image you can use a regular TextureMaterial, however if more, you need to use the dedicated SpriteSheetMaterial
. The difference between the two is that the spritesheet material handles the switching of the sprite sheets automatically for you. Aside managing the diffuse information, it can also handle normal and specular maps for you if your animation requires these extra maps.
Basic example
Download the basic_SpriteSheetAnimation.as
and go to the prepareSingleMap
method.
If you look at the provided maps, for the example, both of them are composed of 4 cells, 8 frames.
In the prepareSingleMap
method, we will assume that our entire animation is stored in one map and contains 4 frames. It can be either loaded externally or embedded and because its one map only, we can declare a single TextureMaterial.
var material:TextureMaterial = new TextureMaterial(Cast.bitmapTexture(testSheet1));
Then we define a name for the animation:
var animID:String = "mySingleMapAnim";
Because internally, the UV coordinates are altered at runtime on the GPU, we need to define a series of coordinates for the playback. The helper class does this for you - details of how it works is explained in the advanced section below. Our simple map is composed of 4 frames only with 2 rows and 2 columns. We need to tell this to the SpriteSheetHelper
helper class, after it has been declared, so it can return the data required for the animator which will be used to drive our spritesheet.
var spriteSheetAnimationSet:SpriteSheetAnimationSet = new SpriteSheetAnimationSet(); var spriteSheetClipNode:SpriteSheetClipNode = spriteSheetHelper.generateSpriteSheetClipNode(animID, 2, 2); spriteSheetAnimationSet.addAnimation(spriteSheetClipNode);
It is now time to declare the animator and we pass the data to the animator.
var spriteSheetAnimator:SpriteSheetAnimator = new SpriteSheetAnimator(spriteSheetAnimationSet);
To display the animation, we need a mesh receiver. In this case a simple plane. and we assign the material.
var mesh:Mesh = new Mesh(new PlaneGeometry(700, 700, 1, 1, false), material);
In this example we use one plane, but the same material could be set on multiple meshes the animation would then play on al of them.
Setting the animator for this object.
mesh.animator = spriteSheetAnimator;
The provided example spritesheet holds only 4 frames, so we set the frame per second very low so we can see the we can see the numbers as it plays and we addchild and start tegh animation.
spriteSheetAnimator.fps = 4; spriteSheetAnimator.play(animID); _view.scene.addChild(mesh);
On render, the plane displays the animation (the left one in the swf)
In the second example, using prepareMultipleMaps
, follows the exact same principles as the first one. This time our information is stored on two maps. We have 8 frames but each map has 4 frames, so we keep the rows and columns 2,2 as above. Make sure to pass the maps in the right order.
var bmd1:BitmapData = Bitmap(new testSheet1()).bitmapData; var texture1:BitmapTexture = new BitmapTexture(bmd1); var bmd2:BitmapData = Bitmap(new testSheet2()).bitmapData; var texture2:BitmapTexture = new BitmapTexture(bmd2); var diffuses:Vector.= Vector. ([texture1, texture2]); //ignore the closing xml tags automatically generated by the wiki engine.
This time we use the SpriteSheetMaterial
as it will handle the swaps for us automatically
var material:SpriteSheetMaterial = new SpriteSheetMaterial(diffuses);
On render, our second plane displays the animation (the right one in the swf).
Intermediate example
This example will show you how to generate your sprite sheets from movieclips embedded into SWF’s and how to use some of the methods to “drive” multiple animators. To illustrate this, the demo shows you how to build a digital clock. Download the intermediate_SpriteSheetAnimation.as
and go to the setUpAnimators
method. This is where the most important functionality is located.
Because our animation is stored in a SWF, we first need to load it and isolate our movieclips that have been placed on stage. They are regular movieclips, with all frames having the same size. The helper class considers the first frame of the movieclip to establish the animation frame size. You could have only one frame to set up the width and height of the frame. Note that in order to show the use of multiple maps, I have set the digits animation to have 60 numbers. The same could be achieved using only the numbers 0-9. In this particular case you would need to have 3 extra animators and 3 additional mesh recievers, so I’ve settled for 0-59 for simplicity. Before starting on the code, note that in order to recreate such content, some modeling knowledge may be required. You need to model each mesh receiver for each digital display as if it were a single object and map its uv’s accordingly. The animator is not aware of the way your model is mapped, and assumes a 0-1 range on both U and V and is uniquely mapped for each face.
The swf stage. All movieclips have an id set, we will use this to retreive the movieclips.
Now our SWF is loaded, lets extract the content.
var loader:Loader = Loader(e.currentTarget.loader); loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, setUpAnimators); var sourceSwf:MovieClip = MovieClip(e.currentTarget.content);
In the example, the source SWF has a movieclip on stage named: “digits”, it will be used for seconds, minutes and hours. The animation holds 60 frames, and is spread over 2 maps wo we’ll have 2 maps of 30 frames each. Columns and rows are defined so we have a full use of the map surface: 6x5.
var animID:String = "digits"; var sourceMC:MovieClip = sourceSwf[animID]; var cols:uint = 6; var rows:uint = 5; var spriteSheetHelper:SpriteSheetHelper = new SpriteSheetHelper(); var diffuseSpriteSheets:Vector.= spriteSheetHelper.generateFromMovieClip(sourceMC, cols, rows, 512, 512, false);
So far, we are using the exact same code as in the previous example but this time, we use the “generateFromMovieClip” method instead. The SpriteSheetHelper
returns us one or more maps from our movieclips. In this case 2 maps of 30 frames.
We do not yet have any geometry to apply the sprite sheets too but we can declare the materials. As they need to be async from each other, we cannot share them in the case of this clock example. We also need to declare 3 different animators, as we will need to drive the time animations independently but reusing the same sprite sheet. One for the hours, one for the minutes and one for the seconds.
_hoursDigits = new SpriteSheetMaterial(diffuseSpriteSheets); _minutesDigits = new SpriteSheetMaterial(diffuseSpriteSheets); _secondsDigits = new SpriteSheetMaterial(diffuseSpriteSheets); var digitsSet:SpriteSheetAnimationSet = new SpriteSheetAnimationSet(); var spriteSheetClipNode:SpriteSheetClipNode = spriteSheetHelper.generateSpriteSheetClipNode(animID, cols, rows, 2, 0, 60); digitsSet.addAnimation(spriteSheetClipNode); _hoursAnimator = new SpriteSheetAnimator(digitsSet); _minutesAnimator = new SpriteSheetAnimator(digitsSet); _secondsAnimator = new SpriteSheetAnimator(digitsSet);
The model mimics a 1980 styled radio clock and on the top is a button with a nice glowing and pulsing animation. Of course, if the model was more interactive, we could let it glow only when the alarm goes off. We reuse the temporary variables and repeat pretty much the same code as we had for the digits, with the only exception being the amount of frames: cols and rows. The animation movieclip has 12 frames, so we set it to 4x3. To make it more interesting, it will loop back and fourth. We also set another property fps
so that a full iteration will take 2 seconds. It will play at different rate as the other animations, yet feel as part of it.
animID = "pulse"; cols = 4; rows = 3; sourceMC = sourceSwf[animID]; diffuseSpriteSheets = spriteSheetHelper.generateFromMovieClip(sourceMC, cols, rows, 256, 256, false); var pulseAnimationSet:SpriteSheetAnimationSet = new SpriteSheetAnimationSet(); spriteSheetClipNode = spriteSheetHelper.generateSpriteSheetClipNode(animID, cols, rows, 1, 0, 12); pulseAnimationSet.addAnimation(spriteSheetClipNode); _pulseAnimator = new SpriteSheetAnimator(pulseAnimationSet); _pulseAnimator.fps = 12; _pulseAnimator.backAndForth = true; _pulseMaterial = new SpriteSheetMaterial(diffuseSpriteSheets);
We do the same for the delimiter. The animation has 10 frames, so we set the rows and cols to 5x2.
animID = "delimiter"; cols = 5; rows = 2; sourceMC = sourceSwf[animID]; diffuseSpriteSheets = spriteSheetHelper.generateFromMovieClip(sourceMC, cols, rows, 256, 256, false); var delimiterAnimationSet:SpriteSheetAnimationSet = new SpriteSheetAnimationSet(); spriteSheetClipNode = spriteSheetHelper.generateSpriteSheetClipNode(animID, cols, rows, 1, 0, sourceMC.totalFrames); delimiterAnimationSet.addAnimation(spriteSheetClipNode); _delimiterAnimator = new SpriteSheetAnimator(delimiterAnimationSet); _delimiterAnimator.fps = 6; _delimiterMaterial = new SpriteSheetMaterial(diffuseSpriteSheets);
Once we are done with our animators, we can now safely load the model as we know our data can be used and set while the file is being loaded. When the assetEvent
event fires we can assign this data to the meshes. You can extract or set the id’s of the meshes in your 3D editor, Prefab3D, AwayBuilder etc…
case "hours": mesh.material = _hoursDigits; mesh.animator = _hoursAnimator; _hoursAnimator.play("digits"); break; case "minutes": mesh.material = _minutesDigits; mesh.animator = _minutesAnimator; _minutesAnimator.play("digits"); break; case "seconds": mesh.material = _secondsDigits; mesh.animator = _secondsAnimator; _secondsAnimator.play("digits"); break; case "delimiter": mesh.material = _delimiterMaterial; mesh.animator = _delimiterAnimator; _delimiterAnimator.play("delimiter"); break; case "button": mesh.material = _pulseMaterial; mesh.animator = _pulseAnimator; _pulseAnimator.play("pulse"); break;
Now you have a completed clock, we can use the Event.ENTER_FRAME
event handler to set the time and update the animators accordingly. As we do not want to update every frame, we check to see if there are any differences in the time between this current one and the previous iteration. Note that during declaration the values were set higher than any possible returned value by the Date
object to ensure that no matter the time of the day your SWF is viewed, that it will update properly and display the correct time. It’s then simply a question of using the gotoAnPlay/Stop as you would do in AS3 with a classic movieclip.
var date:Date = new Date(); if(_lastHour != date.hours+1){ _lastHour = date.hours+1; _hoursAnimator.gotoAndStop(_lastHour); } if(_lastMinute != date.minutes+1){ _lastMinute = date.minutes+1; _minutesAnimator.gotoAndStop(_lastMinute); } if(_lastSecond != date.seconds+1){ _lastSecond = date.seconds+1; _secondsAnimator.gotoAndStop(_lastSecond); _delimiterAnimator.gotoAndPlay(1); }
And voila! We have now our nice digital clock working. I’ll leave to you the additional programming to ge ambient lighting to react to day time/night time and setting the alarm to wake you by playing music!
Adding more animations to the animator
It is possible, if your spritesheet or movieclip holds a series of animations placed sequentially, to define a new animation using the same image source(s). The helper generateSpriteSheetClipNode
method has a from and a to parameter. The changes compared to a single animation would be: a new animation id, the same columns and rows, the same map count as if it would be one. The from and to would define the range within the same source maps. Note that using a gotoAndStop(1) would go to the first frame of this animation which could be the frame XX on the map. You can add as many new sets as you want to the animator, sharing or not the same sources.
Dev note: support for atlas.xml parsing is under dev. Once added, an example will be added to this tutorial.
I hope this tutorial was useful to you, and really looking forward to see your own implementation of the SpriteSheetAnimator
!