Vector Graphics at Runtime

Vector graphics scale well, and are therefore reusable and reduce production time, but they render slowly.

Scaling

Create your vector art at medium size and resize it on the fly as needed. This is a great technique for multiple-screen deployment. Let’s assume your original art was created for an 800×480 device and the application is detecting a tablet at 1,024×600, resulting in a scale difference of about 30%. Let’s scale up the art:

[code]

var dpi:int = Capabilities.screenDPI;
var screenX:int = Capabilities.screenResolutionX;
var screenY:int = Capabilities.screenResolutionY;
var diagonal:Number = Math.sqrt((screenX*screenX)+(screenY*screenY))/dpi;
// if diagonal is higher than 6, we will assume it is a tablet
if (diagonal >= 6) {
myVector.scaleX = myVector.scaleY = 1.30;
}

[/code]

cacheAsBitmap

If the object’s only transformation is along the axes, use cacheAsBitmap:

[code]

myVector.cacheAsBitmap = true;
this.addEventListener(Event.ENTER_FRAME, moveArt);
function moveArt(event:Event):void {
myVector.x += 1;
}

[/code]

cacheAsBitmapMatrix

To rotate, scale, or alpha the object, use cacheAsBitmapMatrix along with cacheAsBit map: Both are required for the caching to work.

[code]

import flash.geom.Matrix;
myVector.cacheAsBitmap();
myVector.cacheAsBitmapMatrix = new Matrix();
this.addEventListener(Event.ENTER_FRAME, onMoveArt);
function onMoveArt(event:Event):void {
myVector.x += 1;
myVector.rotation += 1;
}

[/code]

Vector to Bitmap

If the object is animated with filters or if multiple vector assets need to be composited, draw them into a bitmap. Here, if the device is a tablet (as determined by the diagonal size), we scale the art and apply a drop shadow filter. We then draw the vector art into a bitmap.

[code]

import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.filters.DropShadowFilter;
if (diagonal >= 6) {
myVector.scaleX = myVector.scaleY = 1.30;
}
var shadow:DropShadowFilter = new DropShadowFilter();
shadow.distance = 5;
shadow.angle = 35;
myVector.filters = [shadow];
var bitmapData:BitmapData = new BitmapData(myVector.width, myVector.height);
bitmapData.draw(vector);
var bitmap:Bitmap = new Bitmap(bitmapData);

[/code]

Compositing Vector Graphics

A common use case for vector graphics is the creation of a customized avatar in a virtual world. Discrete parts, such as hair, shirt, and shoes, are separate assets. They can be changed and saved at runtime. The avatar, however, animates best as a whole, so once the avatar information is collected and all the discrete parts are loaded, they can be composited as one single object.

Use the BitmapData.draw method to render vector graphics into a bitmap. The first parameter, the only one required, is the source, bitmap, or vector. The following parameters are a Matrix, a ColorTransform, a blend mode, a Rectangle, and smoothing (only for the BitmapData source).

In this example, we are using the Matrix to scale the assets down to half their original size. Define the dimension of the BitmapData object used to store the pixels of the assets loaded and a Matrix:

[code]

import flash.display.BitmapData;
import flash.geom.Matrix;
const CELL_WIDTH:int = 50;
const CELL_HEIGHT:int = 100;
var composite:BitmapData = new BitmapData(CELL_WIDTH, CELL_HEIGHT, true, 0);
var matrix:Matrix = new Matrix(0.50, 0, 0, 0.50, 0, 0);

[/code]

Create a Vector to store the path of the assets. The art must be loaded in the order it will be composited, as you would with layers in Flash or Photoshop:

[code]

var assets:Vector.<String> = new Vector.<String>;
assets[0] = PATH_SKIN_ASSET;
assets[1] = PATH_HAIR_ASSET;
assets[2] = PATH_SHIRT_ASSET;
var counter:int = 0;

[/code]

Load one image at a time using a Loader object. A counter variable is used to traverse the Vector and load all the assets. Every time one is available in AIR, it is converted to a bitmap and added to BitmapData. Note that the draw method has the alpha argument set to true to preserve the area of the vector without pixels so that it remains transparent:

[code]

import flash.display.Loader;
import flash.net.URLRequest;
import flash.events.Event;
var loader:Loader = new Loader();
loader loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded);
loading();
function loading():void {
loader.load(new URLRequest(assets[counter]));
}
function onLoaded(event:Event):void {
composite.draw(event.target.content, matrix);
counter++;
if (counter < assets.length) {
loading();
} else {
loader.contentLoaderInfo.removeEventListener(
Event.COMPLETE, onLoaded);
display();
}
}

[/code]

Once all the assets are loaded and composited, create a Bitmap to render the image to the screen:

[code]

import flash.display.Bitmap;
function display():void {
var bitmap:Bitmap = new Bitmap(composite);
addChild(bitmap);
}

[/code]

MovieClip with Multiple Frames

Art production may require the use of a movie clip with multiple frames. This is a familiar workflow for many designers while also having the flexibility of being resizable.

Converting a MovieClip with vector art to a bitmap for better rendering is a good practice, but neither cacheAsBitmap nor cacheAsBitmapMatrix works for a MovieClip with multiple frames. If you cache the art on the first frame, as the play head moves, the old bitmap is discarded and the new frame needs to be rasterized. This is the case even if the animation is just a rotation or a position transformation.

GPU rendering is not the technique to use for such a case. Instead, load your Movie Clip without adding it to the display list.

This time, we need to load a single .swf file and traverse its timeline to copy each frame. Load the external .swf file comprising 10 frames:

[code]

var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete);
loader.load(new URLRequest(PATH_TO_SWF));

[/code]

Create a rectangle for the cell at a predefined size. Traverse the MovieClip, and use draw, as before, to copy the vector into a bitmap. Then use copyPixels to add the new pixels into another BitmapData that is the width of the total number of frames:

[code]

function onLoadComplete(event:Event):void {
event.target.removeEventListener(Event.COMPLETE, onLoaded);
var mc:MovieClip = event.target.content as MovieClip;
var totalWidth:int = mc.totalFrames*CELL_WIDTH;
var complete:BitmapData =
new BitmapData(totalWidth, CELL_HEIGHT, true, 0);
var rectangle:Rectangle = new Rectangle(0, 0, CELL_WIDTH, CELL_HEIGHT);
var bounds:int = mc.totalFrames;
for (var i:int = 0; i < bounds; i++) {
mc.gotoAndStop(i+1);
var frame:BitmapData =
new BitmapData(CELL_WIDTH, CELL_HEIGHT, true, 0);
frame.draw(mc, scaleMatrix);
complete.copyPixels(image, rectangle, new Point(i*CELL_WIDTH, 0));
}
frame.dispose();
loader.unloadAndStop(mc);
mc = null;

[/code]

Display the large BitmapData into a Bitmap to render it to the screen:

[code]

var bitmap:Bitmap = new Bitmap(complete);
bitmap.smoothing = true;
addChild(bitmap);

[/code]

Use the reverse process to display a single frame. Here we show the third frame:

[code]

var cellBitmap:BitmapData = new BitmapData(CELL_WIDTH, CELL_HEIGHT, true, 0);
var rectangle:Rectangle =
new Rectangle(CELL_WIDTH*3, 0, CELL_WIDTH+CELL_WIDTH*3, CELL_HEIGHT);
cellBitmap.copyPixels(complete, rectangle, new Point(0, 0));
var bitmap:Bitmap = new Bitmap(cellBitmap);
addChild(bitmap);

[/code]

Next we will cover creating an animation from a sprite sheet bitmap.

 

 

 

The Display List

The structure of your display list is fundamental in this process for three reasons: memory consumption, tree traversal, and node hierarchy.

Memory Consumption

Memory consumption is the trade-off for better performance in GPU rendering because every off-screen bitmap uses memory. At the same time, mobile development implies less GPU memory caching and RAM.

To get a sense of the memory allocated, you can use the following formula:

[code]

// 4 bytes are required to store a single 32 bit pixel
// width and height of the tile created
// anti-aliasing defaults to high or 4 in Android
4 * width * height * anti-aliasFactor
A 10 × 10 image represents 1600 bytes

[/code]

Be vigilant about saving memory in other areas.

Favor the DisplayObject types that need less memory. If the functionality is sufficient for your application, use a Shape or a Sprite instead of a MovieClip. To determine the size of an object, use the following:

[code]

import flash.sampler.*;
var shape:Shape = new Shape();
var sprite:Sprite = new Sprite();
var mc:MovieClip = new MovieClip();
trace(getSize(shape), getSize(sprite), getSize(mc));
// 224, 412 and 448 bytes respectively in the AIR runtime

[/code]

The process of creating and removing objects has an impact on performance. For display objects, use object pooling, a method whereby you create a defined number of objects up front and recycle them as needed. Instead of deleting them, make them invisible or remove them from the display list until you need to use them again.

You should give the same attention to other types of objects. If you need to remove objects, remove listeners and references so that they can be garbage-collected and free up precious memory.

Tree Structure

Keep your display list fairly shallow and narrow.

The renderer needs to traverse the display list and compute the rendering output for every vector-based object. Matrices on the same branch get concatenated. This is the expected management of nested objects: if a Sprite contains another Sprite, the child position is set in relation to its parent.

Node Relationship

This is the most important point for successful use of caching. Caching the wrong objects may result in confusingly slow performance.

The cacheAsBitmapMatrix property must be set on the moving object, not on its container. If you set it on the parent, you create an unnecessarily larger bitmap. Most importantly, if the container has other children that change, the bitmap needs to be redrawn and the caching benefit is lost.

Let’s use an example. The parent node, the black box shown in the following figures, has two children, a green circle and a red square. They are all vector graphics as indicated by the points.

In the first scenario (depicted in Figure 14-2), cacheAsBitmapMatrix is set on the parent node. The texture includes its children. A bitmap is created and used for any transformation, like the rotation in the figure, without having to perform expensive vector rasterization. This is a good caching practice:

[code]

var box:Sprite = new Sprite();
var square:Shape = new Shape();
var circle:Shape = new Shape();
// draw all three items using the drawing API
box.cacheAsBitmap = true;
box.cacheAsBitmapMatrix = new Matrix();
box.rotation = 15;

[/code]

Figure 14-2. Caching and transformation on the parent only
Figure 14-2. Caching and transformation on the parent only

In the second scenario (depicted in Figure 14-3), cacheAsBitmapMatrix is still on the parent node. Let’s add some interactivity to make the circle larger when clicked. This is a bad use of caching because the circle needs to be rerasterized along with its parent and sibling because they share the same texture:

[code]

// change datatype so the display object can receive a mouse event
var circle:Sprite = new Sprite();
// draw items using the drawing API
circle.addEventListener(MouseEvent.CLICK, bigMe);
function bigMe(event:MouseEvent):void {
var leaf:Sprite = event.currentTarget as Sprite;
leaf.scaleX += .20;
leaf.scaleY += .20;
}

[/code]

Figure 14-3. Caching on the parent, but transformation on the children
Figure 14-3. Caching on the parent, but transformation on the children

In the third scenario (depicted in Figure 14-4), cacheAsBitmapMatrix is set, not on the parent, but on the children. When the circle is rescaled, its bitmap copy can be used instead of rasterization. In fact, both children can be cached for future animation. This is a good use of caching:

[code]

// change datatype so they can receive mouse events
var square:Sprite = new Sprite();
var circle:Sprite = new Sprite();
// draw items using the drawing API
square.addEventListener(MouseEvent.CLICK, bigMe);
circle.addEventListener(MouseEvent.CLICK, bigMe);
var myMatrix:Matrix = new Matrix();
square.cacheAsBitmap = true;
square.cacheAsBitmapMatrix = myMatrix;
circle.cacheAsBitmap = true;
circle.cacheAsBitmapMatrix = myMatrix;
function bigMe(event:MouseEvent):void {
var leaf:Sprite = event.currentTarget as Sprite;
leaf.scaleX += .20;
leaf.scaleY += .20;
}

[/code]

Figure 14-4. Caching and transformation on each individual child
Figure 14-4. Caching and transformation on each individual child

The limitation with using GPU rendering occurs when a parent and its children need to have independent animations as demonstrated earlier. If you cannot break the parent- child structure, stay with vector rendering.

MovieClip with Multiple Frames

Neither cacheAsBitmap nor cacheAsBitmapMatrix works for a MovieClip with multiple frames. If you cache the art on the first frame, as the play head moves, the old bitmap is discarded and the new frame needs to be rasterized again. This is the case even if the animation is a rotation or a position change.

GPU rendering is not the technique for such situations. Instead, load your MovieClip without adding it to the display list. Traverse through its timeline and copy each frame to a bitmap using the BitmapData.draw method. Then display one frame at a time using the BitmapData.copyPixels method.

Interactivity

Setting cacheAsBitmapMatrix to true does not affect the object’s interactivity. It still functions as it would in the traditional rendering model both for events and for function calls.

Multiple Rendering Techniques

On Android devices, you could use traditional rendering along with cacheAsBitmap and/ or cacheAsBitmapMatrix. Another technique is to convert your vector assets as bitmaps, in which case no caching is needed. The technique you use may vary from one application to the next.

Remember that caching is meant to be a solution for demanding rendering. It is helpful for games and certain types of animations (not for traditional timeline animation). If there is no display list conflict, as described earlier, caching all assets makes sense. There is no need to use caching for screen-based applications with fairly static UIs.

At the time of this writing, there seems to be a bug using filters on a noncached object while the GPU mode is set in the application descriptor (as in the example below). It should be fixed in a later release:

[code]

var sprite:Sprite = new Sprite();
sprite.graphics.beginFill(0xFF6600, 1);
sprite.graphics.drawRect(0, 0, 200, 100);
sprite.graphics.endFill();
sprite.filters = [new DropShadowFilter(2, 45, 0x000000, 0.5, 6, 6, 1, 3)];
addChild(sprite);

[/code]

Maximum Texture Memory and Texture Size

The maximum texture size supported is 1,024×1,024 (it is 2,048×2,048 for iPhone and iPad). This dimension represents the size after transformation. The maximum memory is not part of the memory consumed by the application, and therefore is not accessible.

2.5D Objects

A 2.5D object is an object with an additional z property that allows for different types of transformation.

If an object has cacheAsBitmapMatrix on and a z property is added, the caching is lost. A 2.5D shape does not need cacheAsBitmapMatrix because it is always cached for motion, scaling, and rotation without any additional coding. But if its visibility is changed to false, it will no longer be cached.

How to Test the Efficiency of GPU Rendering

There are various ways to test application performance beyond the human eye and perception. Testing your frame rate is your best benchmark.