Welcome to this introduction to working at pixel level with ActionScript 3's BitmapData object. We'll take some 2D images, break them into their component pixels, then re-assemble them as 3D images which we can move and rotate.
Final Result Preview
Let's take a look at the final result we will be working towards:
Step 1: Set up
Just before jumping in let's take a moment to look at how the sample project is laid out. Opening the source zip for this tutorial you will have sources for each significant step, you can go right ahead and make a copy of the begin folder as this will serve as our starting point.
Inside this folder you'll find two other folders; src and bin. The src folder is where we will be saving all of our code and FLA files and the bin folder is where the Flash will save the SWF files. Inside the src folder there is the Main.FLA and the Main.AS document class.
If for any reason you find an error in your code, have an attempt to fix it (always good to learn from mistakes) but if you can't then don't worry! You can jump right back in and use one of the steps folders in the source zip that is closest to the step you were on.
Step 2: Download Away3D
If you've already had a peek of the Main.as
file you'll already notice a few references to Away3D, a 3D framework for Flash. We're going to need to download this and add it to our project to continue.
You can grab their latest version from the Away3D site.
Once this download has completed, open the zip file and inside the away3d_3_6_0\src folder you will find three folders, away3d, nochump and wumedia. Copy these, as shown below to your src folder.
Step 3: The Flash File
If you haven't already, open Main.fla
and Main.as
. Looking in the Flash Library you can see an image called 1.png and a MovieClip
with an instance name of img1
, which will serve as a basic container for the png.
We're going to perform a quick compile just to make sure we've added Away3D correctly. If all goes well we should see a blank Flash movie with a dark grey background and no error messages from Flash.
Step 4: The Main.as File
Examining the Main.as
file we can see a few variables that are used in Away3D, there's already a host of tutorials on Away3D but we'll quickly recap on these:
// basic Away3D properties protected var scene:Scene3D; protected var camera:TargetCamera3D; protected var view:View3D;
-
Scene3D
is a space we can use to add 3D objects like cubes and spheres. -
TargetCamera3D
is one of the many types of cameras available in Away3D, it's what we use to look at the Scene3D. -
View3D
is a viewport, often described as the "window" in which we see our scene.
Without going in to specifics you can also see a basic scene is setup ready for use with the initAway3d()
method. Notice it adds an ENTER_FRAME EventListener
, this simply tells Away3D to render()
(or draw) any objects added to the Scene3D
each frame.
/** * Away3D basic scene setup */ private function initAway3d():void { scene = new Scene3D(); camera = new TargetCamera3D({z: -200}); view = new View3D({scene:scene, camera:camera}); addChild(view); addEventListener(Event.ENTER_FRAME, renderLoop); } /** * the render loop */ private function renderLoop(event:Event):void { view.render(); }
That's pretty much it for the introduction to the Main.as
class, we'll be building everything else as we go.
Step 5: Bitmaps and BitmapData
We're going to jump straight in and introduce these two classes, as we'll be working with these throughout the tutorial. If you're new to Bitmap
and BitmapData
you can think of them as a painters canvas and a collection of paint daubs. They're entirely different objects but are both connected, the BitmapData contains all pixel information or brush strokes and would be nothing without being painted on to a canvas or in this case, the Bitmap!
Let's test this out by adding an instance of the img1 MovieClip
to the stage
and making a copy of it using Bitmap/BitmapData
.
Amend the Main.as to the following:
/** * constructor */ public function Main() { initAway3d(); drawExample(); } /** * a quick example of BitmapData and Bitmap usage */ private function drawExample():void { // create an instance of the img1 object on the stage to copy var img:MovieClip = new img1(); addChild(img); // create a BitmapData object with the following parameters: width, height, transparent, color var bmpData:BitmapData = new BitmapData(img.width, img.height, true, 0x000000); // draws a copy of the img MovieClip in to the BitmapData bmpData.draw(img); // adds a Bitmap to the stage with the BitmapData (copy of the img1) information to display var bmp:Bitmap = new Bitmap(bmpData); bmp.y = img.height; addChild(bmp); }
Looking at the drawExample()
code, the first two lines simply add the img1
object to the stage
, this is the image we will make of copy of.
Following that we create a BitmapData
object with the following parameters:
-
width
, the width to make theBitmapData
-
height
, the height to make theBitmapData
-
transparent
, whether theBitmapData
should contain transparent pixels -
color
, the background color
As we know the width and height from img1
we have set them directly, as we're going to need transparency we set the next parameter to true and lastly we specify 0x000000
or black as the background color as it'll appear transparent until we fill it.
Step 6: Bitmaps and BitmapData Continued
Continuing on, now we have the BitmapData
object set up we have several options available to us, we could for example loop through pixel by pixel and copy the image (we'll use something like this later on in the tutorial), or we could use the draw()
method.
The draw()
method takes a MovieClip
or Sprite
as a parameter and will copy all of the pixel information from the object to the BitmapData
.
/** * a quick example of BitmapData and Bitmap usage */ private function drawExample():void { // create an instance of the img1 object on the stage to copy var img:MovieClip = new img1(); addChild(img); // create a BitmapData object with the following parameters: width, height, transparent, color var bmpData:BitmapData = new BitmapData(img.width, img.height, true, 0x000000); // draws a copy of the img MovieClip in to the BitmapData bmpData.draw(img); // adds a Bitmap to the stage with the BitmapData (copy of the img1) information to display var bmp:Bitmap = new Bitmap(bmpData); bmp.y = img.height; addChild(bmp); }
Following this, the next few lines create a Bitmap
object with the BitmapData
pixel information as a parameter, which is then moved below the original img MovieClip
and added to the stage
.
/** * a quick example of BitmapData and Bitmap usage */ private function drawExample():void { // create an instance of the img1 object on the stage to copy var img:MovieClip = new img1(); addChild(img); // create a BitmapData object with the following parameters: width, height, transparent, color var bmpData:BitmapData = new BitmapData(img.width, img.height, true, 0x000000); // draws a copy of the img MovieClip in to the BitmapData bmpData.draw(img); // adds a Bitmap to the stage with the BitmapData (copy of the img1) information to display var bmp:Bitmap = new Bitmap(bmpData); bmp.y = img.height; addChild(bmp); }
There's not a lot of setup involved in setting up the Bitmap
aspect it simply displays a BitmapData, all the magic is with the BitmapData
. Now when testing we should get the following:
Step 7: Reading Pixel Information
Now we have content inside the BitmapData
object things begin to get interesting as we can start manipulating images using getPixel32()
and setPixel32()
.
Starting with getPixel32()
amend the drawExample()
code from above to the following:
/** * a quick example of BitmapData and Bitmap usage */ private function drawExample():void { // create an instance of the img1 object on the stage to copy var img:MovieClip = new img1(); addChild(img); // create a BitmapData object with the following parameters: width, height, transparent, color var bmpData:BitmapData = new BitmapData(img.width, img.height, true, 0x000000); // draws a copy of the img MovieClip in to the BitmapData bmpData.draw(img); // adds a Bitmap to the stage with the BitmapData (copy of the img1) information to display var bmp:Bitmap = new Bitmap(bmpData); bmp.y = img.height; addChild(bmp); // read pixel information from the BitmapData var pixelInformation:uint = bmpData.getPixel32(5, 0); trace(pixelInformation, pixelInformation.toString(16)); }
Examining the code we've created a regular uint variable and assigned it to the value of the pixel in the bmpData
at 5 pixels horizontally and 0 pixels vertically. Remember the values begin at 0 as so:
Knowing that we chose to get the pixel information for 5,0, that would make it black pixel on the top row and sure enough Flash outputs: 4278190080 ff000000
That might not seem right at first, but setPixel32
reads the alpha value of the pixel (where as setPixel
just reads the color). We're generally used to working with hex values for colors such as FFFFFF
or 000000
so we can tell Flash to toString(16)
to get the hex value:
Step 8: Drawing Pixels
Now we know how to read pixel information, drawing pixels to the BitmapData
is very similar, only this time we use setPixel32()
to draw pixels to the BitmapData, and we'll also throw in a for
loop to draw some pixels.
First amend the code to the following:
/** * a quick example of BitmapData and Bitmap usage */ private function drawExample():void { // create an instance of the img1 object on the stage to copy var img:MovieClip = new img1(); addChild(img); // create a BitmapData object with the following parameters: width, height, transparent, color var bmpData:BitmapData = new BitmapData(img.width, img.height, true, 0x000000); // draws a copy of the img MovieClip in to the BitmapData bmpData.draw(img); // adds a Bitmap to the stage with the BitmapData (copy of the img1) information to display var bmp:Bitmap = new Bitmap(bmpData); bmp.y = img.height; addChild(bmp); // read pixel information from the BitmapData var pixelInformation:uint = bmpData.getPixel32(5, 0); trace(pixelInformation, pixelInformation.toString(16)); // write pixel information to the BitmapData var color:uint = 0xffff0000; // ff0000 - full red var row:uint = 0; var column:uint = 0; for(row; row < bmpData.height; row++) { bmpData.setPixel32(column, row, color); column++; if(column > bmpData.width) { column = 0; } } }
The new code starts off creating a regular uint
variable named color
which we store 0xffff0000
which is: ff fully transparent, ff fully red, 00 no green, 00 no blue.
Then there are two counters made for rows and columns (rows are a line of horizontal pixels, columns are a line of vertical pixels). These counters are then put in to a for
loop which increases the row and counter value each time, so when mixed with the setPixel32()
method it will draw a diagonal line:
Step 9: The PixelObject3D Class
In this step we're going to introduce the PixelObject3D.as
class. To save a bit of time grab a copy of the class from the Step 8 folder in the source zip and drop it in to your src
folder besides the Main.fla
and Main.as
.
Once you've done this, lets have a quick look at it before we begin adding the code to create 3D objects from pixels.
// properties protected var _bitmapData:BitmapData = null; public var _scaleFactor:Number = 1; protected var _width:Number = 0; protected var _height:Number = 0;
We have a few protected variables at the top of the class, one for a BitmapData
and three Numbers
for the width, height and a scale of the object.
/** * constructor */ public function PixelObject3D() {} /** * begins the creation process */ public function createFromMovieClip(mc:MovieClip):void { }
Following them is an empty class constructor and the method we will be working with, createFromMovieClip()
. You'll notice this method takes a parameter of MovieClip
type, so as you can already guess we pass it a MovieClip
and it'll give us back a 3D representation of it. When it's finished that is!
Step 10: An Instance of the PixelObject3D Class
While the PixelObject3D.as
class doesn't actually do anything yet let's add an instance of it to the Main.as
class so we can actually see the changes on screen as we go.
Starting with adding a private variable:
// basic Away3D properties protected var scene:Scene3D; protected var camera:TargetCamera3D; protected var view:View3D; // the Pixel3DObject protected var po3d:PixelObject3D;
Following that add to the constructor a call to createPixelObect3D()
.
/** * constructor */ public function Main() { initAway3d(); drawExample(); createPixelObect3D(); }
Lastly add the following function to the Main.as
file. This will create an instance of the PixelObject3D
class, invoke the createFromMovieClip()
method and pass it a new MovieClip
, the img1
we've used previously.
One last line to point out is that we add the PixelObject3D
class as child of the scene as it's a 3D object, not the Stage
.
/** * creates a PixelObject3D */ private function createPixelObect3D():void { po3d = new PixelObject3D(); po3d.createFromMovieClip(new img1()); scene.addChild(po3d); }
Step 11: createFromMovieClip(mc:MovieClip)
Knowing we are passed the MovieClip
we want to recreate from this method, the first thing on our agenda is to make a copy of it using BitmapData exactly as we did before. We can then use the pixel data to begin create 3D objects.
Just as before, we're going to create a BitmapData
object and draw the mc MovieClip
object:
/** * begins the creation process */ public function createFromMovieClip(mc:MovieClip):void { // store references and create the bitmapdata _bitmapData = new BitmapData(mc.width, mc.height, true, 0x000000); _bitmapData.draw(mc); // set width / height _width = mc.width * (2 * _scaleFactor); _height = mc.height * (2 * _scaleFactor); }
We also set the _width
and _height
variables according to the mc
width and height and multiply this by the _scaleFactor
variable, this allows us to scale up or down the size of the 3D pixels if we wish. More on this later.
Step 12: createFromMovieClip(mc:MovieClip)
Remember the BitmapData is only the pixel information and without adding the BitmapData to a Bitmap we won't be able to see it, but we can still read and write to it. This is perfect for us as we're going to use this step to start looping through the pixels of the BitmapData and separating the red, green, blue and alpha values.
Amend your createFromMovieClip()
method to match this:
/** * begins the creation process */ public function createFromMovieClip(mc:MovieClip):void { // store references and create the bitmapdata _bitmapData = new BitmapData(mc.width, mc.height, true, 0x000000); _bitmapData.draw(mc); // set width / height _width = mc.width * (2 * _scaleFactor); _height = mc.height * (2 * _scaleFactor); // pixel information var pixelValue:uint = 0; var red:uint = 0; var green:uint = 0; var blue:uint = 0; var alpha:uint = 0; // loop through each pixel horizontally for (var i:int = 0; i < mc.width; i++) { pixelValue = _bitmapData.getPixel32(i, 0); alpha = pixelValue >> 24 & 0xFF; red = pixelValue >> 16 & 0xFF; green = pixelValue >> 8 & 0xFF; blue = pixelValue & 0xFF; trace("alpha:" + alpha + " red:" + red + " green:" + green + " blue:" + blue); } }
Here we've set up a few variables for the color and alpha values then started a for
loop based on the mc's
width.
This for
loop sets the pixelValue
variable to the value of the current pixel using the getPixel32()
method which we used earlier, but this time note we've used 0
for the second parameter which is y
, so we're only going to process the first horizontal line of pixels.
Following this there is some pretty complex math known as bit masking and shifting, to save a little time you can assume each of the colors is extracted from the pixelValue
variable and then output for us to see using trace()
. If you do want to know more about bitwise operators, bit shifting and masking then you can find a great post at the Polygonal Labs website.
What you should see is the output of a whole bunch of 0
values but pay attention to the two alpha:255
lines, these are the two black pixels at the top of the hand.
Step 13: Creating 3D Objects from the Pixel Values
Phew there was quite a lot of logic in those last few steps! Now we've got the basics up and running, lets start using the pixel information we obtained earlier to create a 3D masterpiece.... almost.
If you've used Away3D or Papervision 3D before you'll be familiar with this step, we're going to start creating 3D cubes and applying materials to them. For every pixel that's alpha is 255 (opaque) we grab its color and create a material based on the color to apply to a 3D cube, below is the code to kick this off:
/** * begins the creation process */ public function createFromMovieClip(mc:MovieClip):void { // store references and create the bitmapdata _bitmapData = new BitmapData(mc.width, mc.height, true, 0x000000); _bitmapData.draw(mc); // set width / height _width = mc.width * (2 * _scaleFactor); _height = mc.height * (2 * _scaleFactor); // pixel information var pixelValue:uint = 0; var red:uint = 0; var green:uint = 0; var blue:uint = 0; var alpha:uint = 0; // loop through each pixel horizontally for (var i:int = 0; i < mc.width; i++) { pixelValue = _bitmapData.getPixel32(i, 0); alpha = pixelValue >> 24 & 0xFF; red = pixelValue >> 16 & 0xFF; green = pixelValue >> 8 & 0xFF; blue = pixelValue & 0xFF; // if pixel is opaque if(alpha == 255) { // create a regular hex color string ie FFFFFF or 000000 var color:String = red.toString(16) + green.toString(16) + blue.toString(16); if(color == "000") color = "000000"; trace("#" + color); // create a material from the color and apply to a 3D cube var material:Material = new ColorMaterial(color); var cube:Cube = new Cube({material:material, width:2 * _scaleFactor, height:2 * _scaleFactor, depth:2 * _scaleFactor}); // position the cube from a - value so registration/transformation point is always center cube.x = 0 - (_width/2) + cube.width * i; this.addChild(cube); } } }
In the above code we've used the red
, green
and blue
variables and created a regular hex color, which you can see output from the trace()
.
Then the hex color color
variable is used to create a ColorMaterial
with Away3D, which is just a regular material based on a color which can be applied to 3D objects.
Following that we create a Cube
object and specify the material
to be the material
object we created the line before it. Also worth noting here is we've set the width
, height
and depth
(remember we're working in three dimensions now!) to a value of twice the value of the _scaleValue
variable, this allows us to make the cubes bigger or smaller by changing _scaleValue
.
Lastly we position the Cube
to zero minus half the width of the mc
multiplied by the for
loops counter i
, this makes the registration or transform point of the finished 3D object in the center. It's then added as a child and when you test you will see two small black 3D Cube
objects.
Step 14: Rows and Columns
Now two 3D cubes is great and all but we really want to get the whole hand shape in to 3D cubes. We're already using a for
loop to loop through all the pixels in the first row, but how do we get it to loop through the remaining rows of pixels?
You guessed it, another for
loop!
/** * begins the creation process */ public function createFromMovieClip(mc:MovieClip):void { // store references and create the bitmapdata _bitmapData = new BitmapData(mc.width, mc.height, true, 0x000000); _bitmapData.draw(mc); // set width / height _width = mc.width * (2 * _scaleFactor); _height = mc.height * (2 * _scaleFactor); // pixel information var pixelValue:uint = 0; var red:uint = 0; var green:uint = 0; var blue:uint = 0; var alpha:uint = 0; // loop through each row of pixels for (var j:int = 0; j < mc.height; j++) { // loop through each pixel horizontally for (var i:int = 0; i < mc.width; i++) { pixelValue = _bitmapData.getPixel32(i, j); alpha = pixelValue >> 24 & 0xFF; red = pixelValue >> 16 & 0xFF; green = pixelValue >> 8 & 0xFF; blue = pixelValue & 0xFF; // if pixel is opaque if(alpha == 255) { // create a regular hex color string ie FFFFFF or 000000 var color:String = red.toString(16) + green.toString(16) + blue.toString(16); if(color == "000") color = "000000"; trace("#" + color); // create a material from the color and apply to a 3D cube var material:Material = new ColorMaterial(color); var cube:Cube = new Cube({material:material, width:2 * _scaleFactor, height:2 * _scaleFactor, depth:2 * _scaleFactor}); // position the cube from a - value so registration/transformation point is always center cube.x = 0 - (_width/2) + cube.width * i; cube.y = (_height/2) + -cube.height * j; this.addChild(cube); } } } }
This time we've only really changed three things, a new for
loop that this time has j
for its counter. The getPixel32()
now has the j
variable added as the y
parameter and lastly the Cube
is positioned vertically using the j
counter.
This pretty much completes the main logic, now it will loop through horizontally, read the pixel values, create a ColorMaterial
and a Cube
and position them accordingly. Once it reaches the end of the horizontal line, because of the new for
loop it will move on to the next pixel down and loop through horizontally again until the image is complete. Have a look for yourself by testing the movie:
Step 15: Into the 3rd Dimension
We now have all of these 3D objects but they're looking very 2D, so we're going to add a bit of movement and get the whole object rotating.
To do this we'll have to back track to the Main.as
file and locate the renderLoop()
method. Remember Away3D will need to render (or paint) the 3D image every frame, so we can add some simple rotations to our PixelObject3D
to see all the child Cubes
rotate:
/** * the render loop */ private function renderLoop(event:Event):void { pObject3D.rotationZ++; view.render(); }
Feel free to experiment with rotationX
, rotationY
and rotationZ
here just remember to reset it back to the code above before continuing. You can also add to the create3DObject()
to better center and align the Cubes
to the camera.
/** * creates a 3D pixel object from a MovieClip */ public function create3DObject():void { pObject3D = new PixelObject3D(); pObject3D.createFromMovieClip(new img1()); pObject3D.x = 80; pObject3D.y = -55; pObject3D.rotationX = -5; scene.addChild(pObject3D); }
Step 16: Exploding the PixelObject3D
Now this is more like it, we can finally see the 3D pixel object rotating. We can begin tweaking this and add an exploded view by simply editing the z
value of the Cubes
when we create them.
Jump back in to the PixelObject3d.as
class and find the lines where we position the Cube
's x
and y
and add the following:
// if pixel is opaque if(alpha == 255) { // create a regular hex color string ie FFFFFF or 000000 var color:String = red.toString(16) + green.toString(16) + blue.toString(16); if(color == "000") color = "000000"; trace("#" + color); // create a material from the color and apply to a 3D cube var material:Material = new ColorMaterial(color); var cube:Cube = new Cube({material:material, width:2 * _scaleFactor, height:2 * _scaleFactor, depth:2 * _scaleFactor}); // position the cube from a - value so registration/transformation point is always center cube.x = 0 - (_width/2) + cube.width * i; cube.y = (_height/2) + -cube.height * j; cube.z = -25 + (Math.random() * 50); this.addChild(cube); }
This will move each Cube
to a random depth from -25 to positive 25 and create a nice exploded effect:
Step 17: Scaling
As the PixelObject3D
is a bit small on the screen, we're going to adjust the scale slightly. We can do this quickly by adjusting the _scaleValue
variable in the PixelObject3D.as
class and increasing it to 1.5.
/** * creates a 3D object from a MovieClip * * @author Anton Mills */ public class PixelObject3D extends ObjectContainer3D { // properties protected var _bitmapData:BitmapData = null; public var _scaleFactor:Number = 1.5; protected var _width:Number = 0; protected var _height:Number = 0;
Step 18: Different Images
Using the PixelObject3D
class to create other images is easy, simply import the image you want to process in to Flash. Then convert it to a MovieClip as per usual, this time give it a Class Name of img2
like this:
Now you can alter Main.as
to use the new img2
object with one tiny alteration:
/** * creates a 3D pixel object from a MovieClip */ public function create3DObject():void { pObject3D = new PixelObject3D(); pObject3D.createFromMovieClip(new img2()); pObject3D.x = 80; pObject3D.y = -55; pObject3D.rotationX = -5; scene.addChild(pObject3D); }
Step 19: Multiple Objects
You can use as many of these as you like, just make sure you add them to the Away3D scene and you could have several. In this example I've removed the z
property that we used in Step 16 for the explosion effect.
Main.as
with another PixelObject3D
added:
/** * A tutorial aimed at introducing ActionScript 3's BitmapData * and how to use the BitmapData information to create a 3D * pixel shape using Away3D. * * @author Anton Mills */ public class Main extends MovieClip { // basic Away3D properties protected var scene:Scene3D; protected var camera:TargetCamera3D; protected var view:View3D; protected var pObject3D:PixelObject3D; protected var pObject3D2:PixelObject3D;
Then create another instance:
/** * creates a 3D pixel object from a MovieClip */ public function create3DObject():void { pObject3D = new PixelObject3D(); pObject3D.createFromMovieClip(new img2()); pObject3D.x = 40; pObject3D.y = -55; pObject3D.rotationX = -5; scene.addChild(pObject3D); pObject3D2 = new PixelObject3D(); pObject3D2.createFromMovieClip(new img1()); pObject3D2.x = 115; pObject3D2.y = -55; pObject3D2.rotationX = -5; scene.addChild(pObject3D2); }
And lastly rotate it in the Away3D render loop:
/** * the render loop */ private function renderLoop(event:Event):void { pObject3D.rotationY++; pObject3D2.rotationY--; pObject3D2.rotationZ--; pObject3D2.rotationX++; view.render(); }
Step 20: Fin.
All that's remaining is for you to test your movie and bask in the wonderfulness of 2D pixels transformed in to 3D objects. Now, what can you do with BitmapData
in your next application or game?
Conclusion
Through this tutorial we've looked at a mixture of items but we've primarily focused on BitmapData
usage such as drawing MovieClips
in to BitmapData
, using setPixel32()
to draw individual pixels, displaying the BitmapData
by using a Bitmap
and reading pixel values using getPixel32()
.
We've also covered some color math, getting hex colors and even individual alpha, red, green and blue values using toString(16)
. Lastly we wrote a small loop to create 3D Cubes
using the pixel values we read, phew!
There's so many possibilities when working at pixel level and with a little imagination and experimentation you can create some really cool applications and games! Thanks for your time, I hope you enjoyed this tutorial.
Comments