14 March 2011
Intermediate experience programming with ActionScript 3 to develop projects with Adobe Flash Builder, as well as experience working with Pixel Bender.
Intermediate
Although the Adobe Pixel Bender kernel language was originally designed to process image and video files, its fast, multi-threaded execution in Adobe Flash Player makes it useful for general math processing. In this tutorial you'll examine a sample project to develop a particle system. You'll learn how to encode general-purpose data for Pixel Bender with a ByteArray, process the data using the ShaderJob class in the Flash runtime, and then return the results of the data.
To get started, create a new project and set up the related files. If you haven't already, be sure to download the sample files folder linked at the beginning of this article. Uncompress the ZIP file and place the folder in a convenient location, such as your desktop. Follow these steps to set up the project:
The next task involves creating a folder in your project named kernels:
To finish the setup process, create the folder that will contain the bytecode file:
The Flash Builder project is now ready to go. In the next section, you'll learn how to add the code to create the first version of the project.
This section shows you how to update the existing code to add the project code. Follow these steps:
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
applicationComplete="applicationCompleteHandler(event)">
<fx:Script>
<![CDATA[
import mx.events.FlexEvent;
private const PARTICLE_COUNT:uint = 8000;
private var m_bytes:ByteArray = null;
// initialize the particle system
private function initializeByteArray():void
{
m_bytes = new ByteArray();
// must be LITTLE_ENDIAN for Pixel Bender
m_bytes.endian = Endian.LITTLE_ENDIAN;
// Create PARTICLE_COUNT particles and store in ByteArray
for ( var i:uint=0; i<PARTICLE_COUNT; i++ )
{
// Particle position - x
m_bytes.writeFloat(Math.random()*particleDisplay.width);
// Particle position - y
m_bytes.writeFloat(Math.random()*particleDisplay.height);
// particle velocity - x
m_bytes.writeFloat((Math.random()*2.0)-1.0);
// particle velocity - y
m_bytes.writeFloat((Math.random()*2.0)-1.0);
}
}
// draw the particle system
private function draw():BitmapData
{
// create the BitmapData object to draw into
var bd:BitmapData = new BitmapData( particleDisplay.width,
particleDisplay.height,
false );
bd.lock(); // lock the BitmapData for drawing
// walk the ByteArray to get all the particles
m_bytes.position = 0;
while ( m_bytes.bytesAvailable > 0 )
{
// the first two floats are the x,y of the particle
// draw using a random color
bd.setPixel( m_bytes.readFloat(),
m_bytes.readFloat(),
0x000000 + Math.random() * 0xFFFFFF );
// skip the next two floats (1 float == 4 bytes),
// they contain the velocity
m_bytes.position += 8;
}
bd.unlock(); // unlock the BitmapData
return bd;
}
protected function applicationCompleteHandler(event:FlexEvent):void
{
// initialize the particle system
initializeByteArray();
// draw the particle system
particleDisplay.graphics.clear();
particleDisplay.graphics.beginBitmapFill(draw());
particleDisplay.graphics.drawRect(0.0,
0.0,
particleDisplay.width,
particleDisplay.height);
particleDisplay.graphics.endFill();
}
]]>
</fx:Script>
<mx:Canvas id="particleDisplay" width="100%" height="100%" />
</s:WindowedApplication>
The code shown above defines three functions, a member variable, and a constant in the <fx:script> block. The constant PARTICLE_COUNT specifies the number of particles that the application will generate. Each particle's position and velocity is stored in the ByteArray named m_bytes. The function initializeByteArray adds four float values for each particle: the x position, the y position, the x velocity, and the y velocity to the m_bytes ByteArray object.
The position is set to a random position within the control. Each velocity value is randomized to a value between –1 and 1. The draw function creates a BitmapData object and then steps through the m_bytes ByteArray object, reading the x and y position values and setting the pixel they represent to a random color.
Finally, the applicationCompleteHandler is an event handler for the applicationComplete event. The applicationCompleteHandler function initializes the m_bytes object and then uses the draw function to display the particle system. Run the ParticleSystemExercise application. You'll see something similar to Figure 1.
In this section, you add the code to set the position of the particles on the Stage. Follow these steps:
evaluatePixel function, after the sampleNearest line, add the following code:// position-new = position-old + velocity
dst.rg += dst.ba;
The Pixel Bender kernel is reading four float values of the ByteArray at a time. The four floating-point values are mapped to the colors in a pixel, so the red and green components of the pixel correspond to the x and y coordinates of the particle and the blue and alpha components of the pixel correspond to the velocity values.
The kernel should now look something like this:
<languageVersion : 1.0;>
kernel Particle
< namespace : "com.adobe.max10";
vendor : "Adobe Systems";
version : 1;
description : "simple non-interacting particle system";
>
{
// the current position and velocities
input image4 src;
// the new position and velocity
output pixel4 dst;
void
evaluatePixel()
{
// get the position/velocity of a particle
dst = sampleNearest(src,outCoord());
// dst.r = particle position X
// dst.g = particle position Y
// dst.b = particle velocity X
// dst.a = particle velocity Y
// position-new = position-old + velocity
dst.rg += dst.ba;
}
}
Here you add the code that animates the particles in the application. Follow these steps:
applicationCompleteHandler function, like this:protected function enterFrameHandler(event:Event):void
{
}
applicationCompleteHandler function, register the new enterFrameHandler function as an event listener for the ENTER_FRAME event and move the lines that draw in the particleDisplay canvas object into the new enterFrameHandler function:protected function applicationCompleteHandler(event:FlexEvent):void
{
initializeByteArray();
this.addEventListener(flash.events.Event.ENTER_FRAME, enterFrameHandler);
}
protected function enterFrameHandler(event:Event):void
{
particleDisplay.graphics.clear();
particleDisplay.graphics.beginBitmapFill(draw());
particleDisplay.graphics.drawRect(0.0, 0.0, particleDisplay.width, particleDisplay.height);
particleDisplay.graphics.endFill();
}
[Embed("pbjs/particle.pbj", mimeType="application/octet-stream")]
private var ParticleKernel:Class;
m_bytes member variable, add a new declaration for a member variable named m_shader of type Shader and initialize it with the Embedded kernel:private var m_shader:Shader = new Shader(new ParticleKernel() as ByteArray);
enterFrameHandler function, add the following lines before the drawing code:m_shader.data["src"].width = PARTICLE_COUNT;
m_shader.data["src"].height = 1;
m_shader.data["src"].input = m_bytes;
var sj:ShaderJob = new ShaderJob( m_shader, m_bytes, PARTICLE_COUNT, 1);
sj.start(true);
This code sets the input of the shader to be the m_bytes ByteArray object. Since the input is a ByteArray and not a BitmapData object, the width and height of the input must be set on the shader object.
In this example, the width is used as the number of particles and the height is set to 1. The ShaderJob object is then created. This requires a Shader object, a destination buffer (in this case the m_bytes ByteArray is reused as the output buffer), and when using a ByteArray or vector, it also requires width and height values of the destination buffer.
When the start function is called, it launches the ShaderJob by passing in the true value and then waits until the ShaderJob completes rather than registering an event handler to be notified when the ShaderJob has completed.
Note: In this case, the name of the input is known, so you can refer to it by name without first checking that it exists. If your project includes code that will load arbitrary Pixel Bender filters, you'll need to enumerate the inputs, rather than attempting to address them directly by their name. Flash includes a limit of 8,000 for both the width and height of inputs.
Run the ParticleSystemExercise application again. The particles animate around the window and, over time, they eventually leave the window. In the next section, you'll resolve that issue.
This section shows you how to add conditional blocks to ensure that the animated particles stay within a specific area. Follow these steps:
float2 parameter named size before the evaluatePixel declaration, like this:parameter float2 size;
Note: In this example, it is not necessary to set the parameter metadata for min, max, and default values.
evaluatePixel function:if ( dst.r < 0.0 )
{
dst.r = 0.0;
dst.b = -dst.b;
}
if ( dst.r > size.x )
{
dst.r = size.x;
dst.b = -dst.b;
}
if ( dst.g < 0.0 )
{
dst.g = 0.0;
dst.a = -dst.a;
}
if ( dst.g > size.y )
{
dst.g = size.y;
dst.a = -dst.a;
}
The conditional blocks displayed above verify that a particle is not outside the window area. If the particle animates beyond one of the edges, it is moved back to the edge and its velocity is inverted, which makes the particle appear to bounce. (Remember that the colors in the pixel are used to refer to the particle's position and velocity as described above.)
Note: You can also refer to the channels of a pixel or float4 value using .xyzw or array indices, which may be helpful if the strategy of using the color letters is confusing.
<languageVersion : 1.0;>
kernel Particle
< namespace : "com.adobe.max10";
vendor : "Adobe Systems";
version : 2;
description : "simple non-interacting particle system";
>
{
// the current position and velocities
input image4 src;
// the new position and velocity
output pixel4 dst;
// the size of the window
parameter float2 size;
void
evaluatePixel()
{
// get the position/velocity of a particle
dst = sampleNearest(src,outCoord());
// dst.r = particle position X
// dst.g = particle position Y
// dst.b = particle velocity X
// dst.a = particle velocity Y
// position-new = position-old + velocity
dst.rg += dst.ba;
// make sure the particle is still in the window
// if it is off an edge, put it back on the edge and
// invert the velocity, making it "bounce"
if ( dst.r < 0.0 )
{
dst.r = 0.0;
dst.b = -dst.b;
}
if ( dst.r > size.x )
{
dst.r = size.x;
dst.b = -dst.b;
}
if ( dst.g < 0.0 )
{
dst.g = 0.0;
dst.a = -dst.a;
}
if ( dst.g > size.y )
{
dst.g = size.y;
dst.a = -dst.a;
}
}
}
enterFrameHandler function:m_shader.data["size"].value = [particleDisplay.width, particleDisplay.height];
This line of code sets the value of the size parameter.
Now it's time to review two strategies for improving performance. The first involves revising the ShaderJob to use multiple threads.
Follow these steps to add the code:
private const ROWS:uint = 10;
enterFrameHandler function, where the width is specified to be PARTICLE_COUNT for both the Shader inputs and ShaderJob output, change that value to PARTICLE_COUNT/ROWS.enterFrameHandler function, change the code where the input height of the shader is 1 and the output height in the ShaderJob is set to 1, to set them to ROWS instead, like this:m_shader.data["src"].width = PARTICLE_COUNT/ROWS;
m_shader.data["src"].height = ROWS;
m_shader.data["src"].input = m_bytes;
m_shader.data["size"].value = [particleDisplay.width, particleDisplay.height];
var sj:ShaderJob = new ShaderJob( m_shader, m_bytes, PARTICLE_COUNT/ROWS, ROWS);
Note: When shaders are executed in Flash Player, the height of the output buffer is used to divide the computation into multiple threads. If the height is set to 1, only one thread is spawned. Most current computers have the capability to run between two and eight threads at the same time. It is a best practice to always use multiple rows when passing ByteArray or vector objects to be processed with Pixel Bender.
The application's performance may not be noticeably different on your computer with only 8,000 particles. Because the data is now being divided into multiple rows, however, it is now possible to modify the PARTICLE_COUNT and ROWS constants so that you can run many more particles without exceeding the width or height of 8,000. If you'd like to experiment, test to see how many you can run and still maintain acceptable performance on your machine.
Now look at the second strategy to improve performance. Follow these steps:
float2 offLeft = float2(0.0, -dst.b);
float2 offLeft2 = dst.rb;
dst.rb = (dst.r < 0.0) ? offLeft : offLeft2;
float2 offRight = float2(size.x, -dst.b);
float2 offRight2 = dst.rb;
dst.rb = (dst.r > size.x) ? offRight : offRight2;
float2 offTop = float2(0.0, -dst.a);
float2 offTop2 = dst.ga;
dst.ga = (dst.g < 0.0) ? offTop : offTop2;
float2 offBottom = float2(size.y, -dst.a);
float2 offBottom2 = dst.ga;
dst.ga = (dst.g > size.y) ? offBottom : offBottom2;
While the logic may seem much more complicated, it is not as complex as it appears. For example, this part of the code:
float2 offLeft = float2(0.0, -dst.b);
float2 offLeft2 = dst.rb;
dst.rb = (dst.r < 0.0) ? offLeft : offLeft2;
can also be written as:
if (dst.r < 0.0)
{
dst.rb = float2(0.0, -dst.b);
}
else
{
dst.rb = dst.rb;
}
Note: Because of the way the Pixel Bender runtime in Flash optimizes code, conditional statements can be significant performance bottlenecks. When you build projects, it is a good idea to test the application to see if the code performed in an if/else block can be computed separately and then use the select statement to assign the final value to variable. This strategy can potentially improve performance, but it is worth evaluating the application using both methods to verify the one that works best.
In the last section of this article, you'll review the completed code that was used to build the final version of the particle system application. If you are following along with these instructions, you can compare your project to the final project files. You can also extend these files to modify the project in various ways.
Compare the completed code for the application with the code that you've updated throughout the course of this tutorial. Open the files you created locally and compare them with the final code shown next.
Final version of the code in ParticleSystemExercise.mxml:
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
applicationComplete="applicationCompleteHandler(event)">
<fx:Script>
<![CDATA[
import mx.events.FlexEvent;
[Embed("pbjs/Particle.pbj", mimeType="application/octet-stream")]
private var ParticleKernel:Class;
private const PARTICLE_COUNT:uint = 80000;
private const ROWS:uint = 10;
private var m_bytes:ByteArray = null;
private var m_shader:Shader =
new Shader(new ParticleKernel() as ByteArray);
// initialize the particle system
private function initializeByteArray():void
{
m_bytes = new ByteArray();
// must be LITTLE_ENDIAN for Pixel Bender
m_bytes.endian = Endian.LITTLE_ENDIAN;
// Create PARTICLE_COUNT particles and store in ByteArray
for ( var i:uint=0; i<PARTICLE_COUNT; i++ )
{
// Particle position – x
m_bytes.writeFloat(Math.random()*particleDisplay.width);
// Particle position – y
m_bytes.writeFloat(Math.random()*particleDisplay.height);
// particle velocity – x
m_bytes.writeFloat((Math.random()*2.0)-1.0);
// particle velocity – y
m_bytes.writeFloat((Math.random()*2.0)-1.0);
}
}
// draw the particle system
private function draw():BitmapData
{
// create the BitmapData object to draw into
var bd:BitmapData = new BitmapData( particleDisplay.width,
particleDisplay.height,
false );
bd.lock(); // lock the BitmapData for drawing
// walk the ByteArray to get all the particles
m_bytes.position = 0;
while ( m_bytes.bytesAvailable > 0 )
{
// the first two floats are the x,y of the particle
// draw using a random color
bd.setPixel( m_bytes.readFloat(),
m_bytes.readFloat(),
0x000000 + Math.random() * 0xFFFFFF );
// skip the next two floats (1 float == 4 bytes),
// they contain the velocity
m_bytes.position += 8;
}
bd.unlock(); // unlock the BitmapData
return bd;
}
protected function applicationCompleteHandler(event:FlexEvent):void
{
// initialize the particle system
initializeByteArray();
this.addEventListener( flash.events.Event.ENTER_FRAME,
enterFrameHandler );
}
protected function enterFrameHandler(event:Event):void
{
// update the particle positions by running them through the
// pixel bender filter this is setting the "width" of the input
// buffer in "pixels" (groups of four floats)
m_shader.data["src"].width = PARTICLE_COUNT/ROWS;
// this is setting the "height" of the input buffer in rows
m_shader.data["src"].height = ROWS;
// this sets the ByteArray as the input data
m_shader.data["src"].input = m_bytes;
// set the size of the window as the value of the size
// parameter
m_shader.data["size"].value = [particleDisplay.width,
particleDisplay.height];
// construct the shader job, using the m_bytes ByteArray as the
// output buffer (it is also being used as the input buffer)
// also pass in the width and height of the input buffer
var sj:ShaderJob = new ShaderJob( m_shader, m_bytes,
PARTICLE_COUNT/ROWS, ROWS);
// launch the shader job and wait for it to return
sj.start(true);
// draw the particle system
particleDisplay.graphics.clear();
particleDisplay.graphics.beginBitmapFill(draw());
particleDisplay.graphics.drawRect( 0.0,
0.0,
particleDisplay.width,
particleDisplay.height );
particleDisplay.graphics.endFill();
}
]]>
</fx:Script>
<mx:Canvas id="particleDisplay" width="100%" height="100%" />
</s:WindowedApplication>
Final version of the code in Particle.pbk:
<languageVersion : 1.0;>
kernel Particle
< namespace : "com.adobe.max10";
vendor : "Adobe Systems";
version : 3;
description : "simple non-interacting particle system";
>
{
// the current position and velocities
input image4 src;
// the new position and velocity
output pixel4 dst;
// the size of the window
parameter float2 size;
void
evaluatePixel()
{
// get the position/velocity of a particle
dst = sampleNearest(src,outCoord());
// dst.r = particle position X
// dst.g = particle position Y
// dst.b = particle velocity X
// dst.a = particle velocity Y
// position-new = position-old + velocity
dst.rg += dst.ba;
// make sure the particle is still in the window
// if it is off an edge, put it back on the edge and invert the velocity, making it "bounce"
// instead of using if statements, use conditional assignments instead which are MUCH faster
// in the Flash Pixel Bender runtime.
//
// conditional assignments in Flash only let you choose between two constants, so you
// need to compute both values, but the computation here is trivial so it isn't a big performance
// hit
//
// also using the ability to set multiple values of a vector at once to reduce the number of operations
float2 offLeft = float2(0.0, -dst.b);
float2 offLeft2 = dst.rb;
dst.rb = (dst.r < 0.0) ? offLeft : offLeft2;
float2 offRight = float2(size.x, -dst.b);
float2 offRight2 = dst.rb;
dst.rb = (dst.r > size.x) ? offRight : offRight2;
float2 offTop = float2(0.0, -dst.a);
float2 offTop2 = dst.ga;
dst.ga = (dst.g < 0.0) ? offTop : offTop2;
float2 offBottom = float2(size.y, -dst.a);
float2 offBottom2 = dst.ga;
dst.ga = (dst.g > size.y) ? offBottom : offBottom2;
}
}
Adapt the Pixel Bender concepts described in this article and apply them to your own ActionScript projects. Experiment with extending this sample project by adding a second source input to the Pixel Bender kernel that uses an actual bitmap image to constrain the particles.
Check out these other resources to learn more about working with Pixel Bender:
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. Permissions beyond the scope of this license, pertaining to the examples of code included within this work are available at Adobe.
Tutorials & Samples |
| 04/10/2012 | Fast Fourier Transform (FFT) in Pixel Bender? |
|---|---|
| 04/19/2012 | Shader works in toolkit, not in Flash |
| 04/12/2012 | Pixel Bender removed from AE CS6 !? |
| 03/22/2012 | Load an HDR image into Pixel Bender Toolkit |