31st March 2012
In the last article I mentioned how reading and writing of the data from and to bitmap form could be a significant bottleneck, and a potential drawback to the filter computation method. It's clearly desirable to minimise the number of accesses needed; here I describe a method I've discovered which can contribute to this goal.
One of the main usage cases for filter-based code is the manipulation of some two-dimensional array data -
either for some overall environmental process or object manipulation (which I will consider in more
depth in a later article).
Naively, when it comes to rendering this data these are the possibilities which avoid tapping the full state :
- The data is not displayed, merely used via appropriately placed taps
- The data is displayed as pixels or plain rectangles
- The data is read out over a small area, and plotted in a more attractive format.
All of these are satisfactory under particular circumstances. Good uses of these could be for wind effects, falling sand or to give a detailed closeup of a much larger simulation.
However, would it not be nice to render each datapoint as a bitmap graphic?
Here is an example, using a simple 6-state CA. :
Simple 6-state Cellular Automata with filter sprites
Click on-screen to draw or remove walls, and use the mouse menu to change iteration speed or randomise the display
This process relies on the highly adaptable displacementMapFilter function.
This is normally used to apply a distortion effect to a bitmap, for example ripples in water. It uses two channels from a second supplied bitmap to offset the pixel read for each position. Here we use it to select which image to display for the tile.
In overview, the pathway is essentially as follows:
- convert the data to be plotted into spritesheet offsets
- add on a 'correction factor' bitmap to align each tile to the spritesheet
- expand the data to flat rectangles of the desired size
- lookup display pixels from the spritesheet
The displacementMapFilter uses a channel to describe the offset of the read pixel in each
direction; a value in the range 0..255 represents an offset of -128..127.
For reasons described in the next section, we can only use half of this range, giving a maximum size for the spritesheet of 128x128 pixels.
The displacementMapFilter only processes the union of the bitmaps supplied, so this needs to be repeated to cover at minimum the size of the display area.
Correction factor bitmap
As the pixel selection is a displacement from the initial position, the range of
the source image (the spritesheet) targetted depends on the image coordinate.
The offset into the spritesheet has to be the same for each tile position.
We can arrange this by adding on an offset dependent on the position.
Fortunately, flash provides the ability to combine pixel values together directly, using alternative blendmodes. Using the 'add' mode, we can add on 128 for the lefmost/topmost tile, then step down by one tile's width/height at a time.
With this strategy we can have a two-dimensional table of up to 128x128 pixels. Again this needs to be repeated to cover the display area when scaled up.
Processing the data
So far we've considered only initialisation - these bitmaps can be generated and
stored as constant data. What follows needs to be done per frame.
First we take a copy of the correction-factor bitmap.
Generally, the working data will need to be copied and processed into spritesheet offsets. In some cases the values may simply be scaled up with a colorTransform or colorMatrixFilter operation, but using paletteMap is the most flexible, and can sometimes be had for free - if there are two unused channels in the internal representation of each cell.
Onto this tile data the correction factor bitmap is drawn with a blendmode of 'add', then the result is scaled up so each pixel 'cell' becomes a bitmap 'tile'. The displacementMapFilter then uses the resulting bitmap as its distortion map, and the spritesheet as source bitmap, converting each flat-filled rectangle into an image drawn from the spritesheet.
This covers the basic method for rendering filter sprites. Obviously there are several very clear restriction on these graphics - the 128 by 128 size addressing limit and grid alignment are the most egregious examples. However, more advanced and specialised techniques can overcome either of these, or any other particularly objectionable limit.
Please note that filter sprites are still only going to be a win under a fairly limited set of conditions. In particular, there needs to be a good reason for redrawing everything - if a large proportion of the images don't change then the rendering effort for them is wasted. On the other hand, though, at least the processing power required is a constant, so there won't be intermittent lag however much activity occurs. Also, the smaller the tiles the better - the cost of reading data out and rendering each individually with copyPixels increases greatly, while the filter sprites method has constant cost. And not only that - there is space for more tiles within the size limit.
Some of the difficulties imposed by filters can be overcome, with some ingenuity.
Graphical overview of the filtersprites method:
- draw~blendmode add : accumulate two values (for other computationally useful functions see also lighten, darken, difference and subtract, among other drawing modes)
- displacementMapFilter : select a value from a two-dimensional lookup table