# Working at Pixel Level with BitmapData and Away3D

##### Tutorial Details
• Difficulty: Intermediate
• Platform: Flash (Flash Player 10)
• Language: AS3
• Libraries used: Away3D
• Software used: Flash Professional CS5
• Estimated Completion Time: 30 mins

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.

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.

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});

}

/**
* 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();

// 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;
}


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 the BitmapData
• height, the height to make the BitmapData
• transparent, whether the BitmapData 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();

// 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;
}


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();

// 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;
}


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();

// 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;

// 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();

// 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;

// 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());

}


## 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;

}
}
}


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 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;

}


## 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 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;

}


## 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;

pObject3D2 = new PixelObject3D();
pObject3D2.createFromMovieClip(new img1());

pObject3D2.x = 115;
pObject3D2.y = -55;
pObject3D2.rotationX = -5;

}


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.

• http://www.rayomandpavri.com rayo

awsome….looking fwd to more tutorials explained with such detail and simplicity…..thank you.

• http://snaptin.com Ian Yates
Staff

Yep – it’s great to have Anton as part of our regular writing team :)

• http://www.mindcookin.com Yago

Agree 100% with rayo, great tutorial!!!

• gypark

Maybe it’s alpha-red-green-’blue’ on second image in step 7.

Great Work! :D

• http://irie-design.fr yorote

meeeh a new good tut to do :). Thanks A Lot!

• Ric

Great and very interesting tutorials Anton! Looking forward for your next one!!
Thanks

• http://konturart.net kontur

Great tutorial! Concrete and simple, yet abstract enough to learn and apply to some own idea.