by Joe Ward

Joe Ward

Created

2 February 2009

 Requirements

Prerequisite knowledge

General experience building applications with Flex Builder is suggested. The examples in this article were built with Flex Builder and the Flex 3.2 SDK.

 

User level

Intermediate

Required products

Flash Player 10

Flex 3 (Download trial)

 
Additional Requirements

 
Pixel Bender Toolkit
 
Sample files:
Note: The examples in this article include the following projects: ChannelScrambler, CheckerFill, GaussianBlur, GrainBlend, HardLightBlend, MultiplyBlend, and ScreenBlend. The SWF content for the examples are displayed in this article. To examine the examples more closely, download the sample files and open the files in the Pixel Bender Toolkit and Flex Builder.
 
Pixel Bender is a graphics processing engine supported by Adobe Flash Player 10, Adobe After Effects, and (soon) Adobe Photoshop. The language is based on fragment shader languages, such as OpenGL Shading Language (GLSL), used to optimize pixel drawing operations in 3D rendering. In Flex, you can use Pixel Bender programs to create filters, blends, area fills, and line fills.
 
Pixel Bender effects can be applied to any display object, including images, vector graphics, and even digital video. The execution speed is extremely fast; effects that would have taken seconds per frame to execute in ActionScript can now be achieved in real time (see Figure 1)
 
LayoutImage
Figure 1. Pixel Bender effects can be applied to any display object.
 
Pixel Bender programs, called kernels, are written and compiled using the Adobe Pixel Bender Toolkit. The compiled bytecode produced by the toolkit can be loaded into a Shader object and used by your SWF content. The Pixel Bender Toolkit is installed automatically when you install Flash CS4 Professional.
 
Documentation for using Pixel Bender in Adobe Flex Builder is available in the Programming ActionScript 3.0 chapter Working with Pixel Bender shaders and in the ActionScript Component and Language Reference in the Shader class section. This documentation contains a detailed description of the objects you can use with Pixel Bender in Flex and Flash. Documentation on the Pixel Bender language is available from the Help menu of the Pixel Bender Toolkit.
 
If you want to see what's possible, as well as learn some new tricks, I recommend reviewing examples of Pixel Bender programs. In addition to the examples provided in this article, you can find a public repository of kernels hosted by Adobe at the Pixel Bender Exchange. The authors of these kernel programs have kindly agreed to share them with the wider Flash developer community. Perhaps your kernel will be the next one posted? There is also a forum for discussing Pixel Bender programming. Also visit the Pixel Bender Technology Center.
 

 
Pixel Bender language overview

Before jumping into kernel coding, take a brief look at the Pixel Bender language. You probably won't become an expert in the language just from reading this article, but the following discussion should help you understand programs written in the Pixel Bender language. For far more detail, refer to the Adobe Pixel Bender Language 1.0 Tutorial and Reference Guide, available from the Help menu of the Pixel Bender Toolkit application.
 
Pixel Bender uses procedural syntax similar to languages such as C, Java, and ActionScript. It includes built-in data types and functions targeted at image processing. If you are already familiar with ActionScript, then learning to write Pixel Bender kernels should be reasonably straightforward. The main differences in syntax between ActionScript and Pixel Bender language include:
 
  • Type declarations for variables go in front of the variable name, instead of after it. The var keyword is not used. Thus, instead of declaring an integer value with:
var foo:int;
you use the syntax:
 
int foo;
  • New objects cannot be created at runtime. Thus, the new keyword is not supported (or needed).
  • Pixel Bender uses the data type, float, instead of the data type, Number, for representing real numbers. The term float stands for floating-point (which just means that the decimal point can "float" to any position in the number).
  • It is important to remember that when you type a floating point number in a Pixel Bender program, you must include a decimal point. Otherwise, the Pixel Bender compiler will treat the number as an integer rather than a float value. Unlike ActionScript, Pixel Bender does not do any implicit data conversion. So if, for example, you type 1 instead of 1.0, you will get an abstruse error message from the Pixel Bender Toolkit.
  • Pixel Bender includes built-in vector data types. A vector type can be recognized by the number appended to the base type name. For example, a float3 type is a vector containing three elements of type float.
  • The following statement illustrates a typical use of a vector data type in Pixel Bender:
float4 rgbaPixel = float4( 1.0, 0.3, 0.2, 0.8 );
  • The statement above declares a new vector variable named rgbaPixel and assigns it a color value. The expression, float4( 1.0, 0.3, 0.2, 0.8 ), defines a literal float4 vector constant, which is assigned to the rgbaPixel variable.
  • Pixel Bender supports vector swizzling.
The members of a vector can be accessed using dot notation and one of three sets of elements names: r,g,b,a; x,y,z,w; or s,t,p,q. Swizzling lets you rearrange the elements simply by reordering the element names. For example, the following statement swaps the red and green color channels of a pixel vector when assigning the pixel value to another variable:
 
pixel4 mixedUp = rgbaPixel.grba;
You can also repeat channels:
 
pixel4 allRed = rgbaPixel.rrra;
and drop channels:
 
pixel2 redAndBlue = rgbaPixel.rb;
Note: The choice of which set of element names to use with a vector variable is up to you. For example, myVector.r is the same as myVector.x. A good practice is to use the rgba set for colors, and the xyzw or stpq sets for positions. You cannot mix names from more than one set in the same reference.
 
  • Pixel Bender also supports built-in matrix data types. A matrix data type is similar to a vector, but contains a two dimensional array of numbers. The float3×3 matrix type, for example, contains three vectors of three elements.
  • Pixel Bender in Flash Player does not support loops, custom functions (other than the evaluatePixel() function), or arrays.
Note: When developing Pixel Bender kernels for Flash Player or AIR, be sure to enable the Turn on Flash Player Warnings and Errors option (under the Build menu of the toolkit window). With this option enabled, the compiler will inform you immediately when you are using unsupported Pixel Bender language features. (Otherwise, the toolkit won't report the errors until you try to export the kernel for Flash Player.)
 
 
Kernel walkthrough
The typical Pixel bender kernel performs the following tasks:
 
  • Samples a pixel from the input image
  • Applies a calculation to the sampled pixel color
  • Assigns the modified value to the output pixel
The following simple Pixel Bender kernel does each of these tasks. The program defines a kernel named ChannelScrambler:
 
<languageVersion : 1.0;> kernel ChannelScramblerFilter < namespace : "com.adobe.example"; vendor : "Adobe Systems Inc."; version : 1; description : "Changes the color channel order from rgba to gbra."; > { input image4 inputImage; output pixel4 outPixel; void evaluatePixel() { pixel4 samplePixel = sampleNearest( inputImage, outCoord() ); outPixel = samplePixel.gbra; } }
The kernel declares an input image, named inputImage, and an output pixel, named outPixel. In the evaluatePixel() function, the pixel at the output coordinates currently being processed is accessed using two built-in functions, sampleNearest() and outCoord(). Next, the sampled pixel is assigned to the outPixel variable, using swizzling to reorder the color channels.
 
This kernel produces the following result shown in Figure 2.
 
LayoutImage
Figure 2. The result of the Channel Sampler.
 
The required elements of a Pixel Bender program include the languageVersion tag:
 
<languageVersion: 1.0;>
and the kernel name declaration:
 
kernel ChannelScrambler{...}
Inside the kernel, there must be a single output declaration, such as output pixel4 outPixel, and an evaluatePixel() function. A kernel may have any number of inputs, including no inputs at all. However, the way you use a kernel in Flex creates additional requirements on the number of inputs. A shader used as a blend requires two inputs, a shader used as a filter requires one input, and a shader used as a fill does not require any inputs at all.
 
A Pixel Bender kernel is run once for every pixel in the output image. No state information is saved between runs, so it isn't possible to collect an average pixel value by accumulating the pixels sampled in each run. Each run of the kernel must compute all the information it needs—although you can pre-compute information in ActionScript and pass it in as an input or parameter.
 
 
Sampling
To access the pixel values in an input image, you must use the sampling functions. Sampling is accomplished with the following built-in functions:
 
  • sampleNearest() returns a vector containing the channel values of the pixel closest to the specified coordinates. (There is also a sampleLinear() function that behaves slightly differently.)
  • outCoord() returns the coordinates of the current output pixel.
As Pixel Bender processes an image, it executes the kernel for every pixel in the output image. The outCoord() function returns the coordinates of the current pixel. The Pixel Bender coordinate system is similar to that used by Flash Player and AIR. The origin is registered at the top, left corner. Positive values increase to the right and down. Pixels are always square.
 
You are not limited to sampling the pixel directly at the outCoord() position. For example, you could use the following sampling statement to sample the pixel that is 10 pixels right and 5 pixels down from the current pixel:
 
pixel4 inputPixel = sampleNearest( inputImage, outCoord() + float2( 10.0, 5.0 ) );
The expression, outCoord() + float2( 10.0, 5.0 ), adds a two-element vector to the vector of coordinates produced by the outCoord() function. An equivalent way to code the same expression is: float2(outCoord().x + 10.0, outCoord().y + 5.0).
 
If the sample coordinates go beyond the bounds of the input image, then a color vector containing all zeros is returned. For example, if the input is of type image4, then sampling an exterior pixel will return a completely transparent black pixel. If the input is of type image3, then it will return a black pixel (without an alpha channel). The Pixel Bender coordinate space theoretically extends to infinity. There are, of course, practical limits on the range of coordinates that can be expressed, as well as limits on the usefulness of sampling data that does not exist.
 

 
Working with pixels

Once you have a vector representing a pixel, there are a number of ways to work with the color values. For example, if you have a pixel4 variable named pix, you can address the individual color channels, or combinations of channels in the following ways:
 
  • Red channel: pix.r or pix[0]
  • Green channel: pix.g or pix[1]
  • Blue channel: pix.b or pix[2]
  • Alpha channel: pix.a or pix[3]
  • Red and alpha channels: pix.ra
  • All the color channels with red and blue swapped and no alpha channel: pix.bgr
A single color channel is a 32-bit floating-point number, normally between 0.0 (black) and 1.0 (white). The output color values can be outside this range, but they won't change the rendered appearance. In other words, pixel3(-1.0, -1.0, -1.0) is just as black as pixel3(0.0, 0.0, 0.0) when rendered as a bitmap. (However, if you run multiple filters on the same image, the difference can be significant since the second filter will see (-1.0, -1.0, -1.0) not (0.0, 0.0, 0.0) when sampling that pixel.)
 
You can perform arithmetic on a pixel vector with either scalar or vector values. When using scalar values, the operation is applied to each channel. For example, the following operation will divide the value of each channel in half (including the alpha channel):
 
pixel4 pix = sampleNearest( inputImage, outCoord()); pix = pix / 2.0;
The same operation could also be written using vectors:
 
pix = pix * pixel4( 0.5, 0.5, 0.5, 0.5 );
Although Pixel Bender images have 32-bits per channel, graphics in Flash Player and AIR only have 8-bits per channel. When a kernel is run, the input image data is converted to 32-bits per channel and then converted back to 8-bits per channel when kernel execution is complete.
 
 
Defining inputs
Inputs are declared with the input keyword:
 
input image4 sourceImage;
Inputs can be declared using the data types: image1, image2, image3, or image4. The different inputs in a kernel can have different numbers of channels. The number of channels in the output image produced by the shader is determined by the data type of the output pixel, not by the data types of the inputs.
 
You can declare more inputs than are required for the way a kernel is used in Flex. However, the Flash Player or AIR runtime will only automatically assign image data to the required inputs. You must assign an image to the extra inputs before assigning the kernel as a blend, filter or fill. For example, if your filter kernel used an additional image to create a textured effect, you would have to assign the texture as an input to the kernel before assigning the ShaderFilter containing the kernel to the filter array of a display object (more on how to do that later in this article).
 
 
Defining parameters
In addition to input images, you can supply other values to a kernel as parameters. Parameters are declared with the parameter keyword and can be any data type, except image (or region—which you can't use in kernels written for Flash Player or AIR anyway). You can declare metadata for parameters to specify the default, minimum, and maximum values. You can also supply a description. The metadata is declared between angle braces (<>) and can be accessed in ActionScript code. It is always a good idea to define a reasonable default value for a parameter.
 
The following parameter statement declares a float3 parameter with metadata:
 
parameter float3 weights < defaultValue : float3( 0.5, 0.5, 0.5 ); minValue : float3( 0.1, 0.1, 0.1 ); maxValue : float3( 0.9, 0.9, 0.9 ); description : "A three element vector of weight values." >;
To access parameter values in ActionScript code, you use the data property of the ActionScript Shader object containing the kernel. The current, minimum and maximum values of the above parameter can be accessed in ActionScript with the following statements (assuming that myShader is a Shader object containing a kernel with this parameter):
 
var currentWeights:Array = myShader.data.weights.value; var minWeights:Array = myShader.data.weights.minimumValue; var maxWeights:Array = myShader.data.weights.maximumValue;
Since the parameter is a float3 vector type, the ActionScript arrays returned will contain three elements. If the parameter was a scalar type, such as float, then the arrays returned would contain a single element.
 

 
Exporting and loading a kernel

Use the Export Kernel Filter for Flash Player command from the Pixel Bender Toolkit to compile and export the kernel for use in the Flash Player and AIR runtimes. Kernels are exported with a file extension .pbj (see Figure 3).
 
Exporting the kernal from the Pixel Bender Toolkit
Figure 3. Exporting the kernal from the Pixel Bender Toolkit
To load a Pixel Bender kernel in a Flex application, you must either embed or load the compiled kernel.
 
The Embed tag instructs the ActionScript compiler to embed the Pixel Bender kernel when it creates the SWF file. You must include the MIME type declaration, as shown in the following example:
 
[Embed(source="channelscrambler.pbj", mimeType="application/octet-stream")] var ChannelScramblerKernel:Class;
To use the kernel, create an instance of the class, in this case, ChannelScramblerFilter. The following code uses an embedded kernel to create new Shader and ShaderFilter objects, which are applied it to an Image instance:
 
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" applicationComplete="applyFilter()" width="476" height="281" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]"> <mx:Script> <![CDATA[ //Embed the PixelBender kernel in the output SWF [Embed(source="kernels/channelscrambler.pbj", mimeType="application/octet-stream")] private var ChannelScramblerKernel:Class; private function applyFilter():void { var shader:Shader = new Shader(new ChannelScramblerKernel() ); var shaderFilter:ShaderFilter = new ShaderFilter( shader ); camellia.filters = [ shaderFilter ]; } ]]> </mx:Script> <mx:HBox width="410" height="100%"> <mx:VBox height="100%"> <mx:Image source="images/camellia.jpg" scaleContent="false" autoLoad="true"/> <mx:Label text="Original" width="200" textAlign="center"/> </mx:VBox> <mx:VBox height="100%"> <mx:Image source="images/camellia.jpg" id="camellia" scaleContent="false" autoLoad="true"/> <mx:Label text="Channel Scrambler" width="199" textAlign="center"/> </mx:VBox> </mx:HBox> </mx:Application>
Using the Embed tag is typically the simplest method of loading Pixel Bender kernels, but you can also load kernels at runtime. The following example shows how to use the URLLoader class to load a kernel:
 
function loadFilter() { var urlRequest:URLRequest = new URLRequest( "channelscrambler.pbj" ); var urlLoader:URLLoader = new URLLoader(); urlLoader.dataFormat = URLLoaderDataFormat.BINARY; urlLoader.addEventListener( Event.COMPLETE, applyFilter ); urlLoader.load( urlRequest ); } function applyFilter( event:Event ):void { trace("apply"); urlLoader.removeEventListener( Event.COMPLETE, applyFilter ); var shader:Shader = new Shader( event.target.data ); var shaderFilter:ShaderFilter = new ShaderFilter( shader ); camellia.filters = [ shaderFilter ]; }

 
Using Pixel Bender kernels in AIR applications

You can use Pixel Bender kernels in an AIR application exactly as you would in a browser-targeted application. If you load a kernel dynamically, the kernel must be included in the application package. In Flex Builder, kernel files in the source directory are typically included automatically when you export the AIR file. (If you embed the kernel, it is already included in the application SWF file, so it doesn't need to be added to the AIR package.)
 
The examples in this article are all targeted at the browser so that you can view the examples live. To convert one of the examples to an AIR application, you just need to create an application descriptor. For example, the following shows a minimal application descriptor for the ChannelScrambler example:
 
<?xml version="1.0" encoding="UTF-8"?> <application xmlns="http://ns.adobe.com/air/application/1.5"> <id>com.adobe.examples.flex.ChannelScrambler</id> <version>1.5</version> <filename>Channel Scrambler</filename> <name>Channel Scrambler -- Flex-based AIR Example</name> <description>Illustrates how to use Pixel Bender Kernels in Flex and AIR.</description> <copyright>© 2007 Adobe Systems Inc.</copyright> <initialWindow> <title>Channel Scrambler</title> <content>ChannelScrambler.swf</content> <systemChrome>standard</systemChrome> <transparent>false</transparent> <visible>true</visible> </initialWindow> <installFolder>Adobe/AIR Examples</installFolder> <programMenuFolder>Adobe/AIR Examples</programMenuFolder> </application>
Note that the namespace specified in the xmlns attribute must target AIR 1.5. If you target 1.0 or 1.1, only Flash Player 9 APIs will be available to the application.
 

 
Using blends

A blend combines the colors in the display object to which the blend is applied with the colors below the object on the Stage. The Flash Player API supports several built-in blends, defined in the BlendMode class. As a learning exercise, we duplicate a couple of the built-in blends. Then we create a blend that can't be easily achieved using the built-in options.
 
To apply a blend to a display object, create a Shader object with the loaded kernel bytecode and assign it to the blendShader property of the display object. A blend kernel must have two inputs. The first input is the foreground display object (whose blendShader property is set). The second input is whatever is behind the foreground object. If you use additional inputs, perhaps for creating masks or textures, you must assign an image in the form of a BitmapData object to these inputs yourself before applying the blend.
 
 
Multiply
In a multiply blend, each color in the foreground object is multiplied by the color of the background object. This blend darkens the result, except in the scenario where one of the images is pure white as shown in Figure 4.
 
LayoutImage
Figure 4. Pixel Bender effects can be applied to any display object.
 
The following kernel declares two inputs, named foreground and background, and an output, named result. In the evaluatePixel() function, the pixel at the current coordinate is sampled in each image using the sampleNearest() function. The pixels are then multiplied together.
 
<languageVersion : 1.0;> kernel MultiplyBlend < namespace : "com.adobe.example"; vendor : "Adobe Systems Inc."; version : 1; description : "A simple multiply blend"; > { input image4 foreground; input image4 background; output pixel4 result; void evaluatePixel() { pixel4 a = sampleNearest( foreground, outCoord() ); pixel4 b = sampleNearest( background, outCoord() ); result = a * b; } }
Note: When you multiply two vectors in the Pixel Bender language, each corresponding pair of component values are multiplied together. Thus, if a and b are float2 vectors, for example, the statement:
 
a * b
is equivalent to the two statements:
 
a.x * b.x a.y * b.y
The following ActionScript code is used to load and apply the blend:
 
<mx:Script> <![CDATA[ //Embed the Pixel Bender kernel in the output SWF [Embed(source="kernels/multiplyblend.pbj", mimeType="application/octet-stream")] private var MultiplyBlendKernel:Class; private function applyBlend():void { var shader:Shader = new Shader( new MultiplyBlendKernel() ); camellia.blendShader = shader; } ]]> </mx:Script>
 
Screen
A screen blend inverts the colors, multiplies them together, and then inverts the result. This has the opposite effect as applying the multiply blend. The screen blend lightens the result, except in the scenario where one of the images is black (see Figure 5).
 
LayoutImage
Figure 5. The screen blend lightens the result.
 
<languageVersion : 1.0;> kernel ScreenBlend < namespace : "com.adobe.example"; vendor : "Adobe Systems Inc."; version : 1; description : "Screen blend"; > { input image4 foreground; input image4 background; output pixel4 result; void evaluatePixel() { pixel4 a = sampleNearest( background, outCoord() ); pixel4 b = sampleNearest( foreground, outCoord() ); result = 1.0 - (1.0 - a) * (1.0 - b); } }
As you can see, this kernel is almost identical to the multiply kernel. Only the mathematical operation used to produce the result has changed. This example uses the same ActionScript code, except it is loading and applying a different shader.
 
 
Hard light
A hard light blend is a combination of the multiply and screen blends. If the foreground pixel is lighter than 50% gray, then a screen blend is performed. Otherwise, a multiply blend is performed (see Figure 6).
 
LayoutImage
Figure 6. A hard light blend is a combination of the multiply and screen blends.
 
<languageVersion : 1.0;> kernel HardLightBlend < namespace : "com.adobe.example"; vendor : "Adobe Systems Inc."; version : 1; description : "Hard light blend"; > { input image4 foreground; input image4 background; output pixel4 result; void evaluatePixel() { pixel4 a = sampleNearest( background, outCoord() ); pixel4 b = sampleNearest( foreground, outCoord() ); float gray = (b.r + b.g + b.b)/3.0; if( gray < 0.5 ) { result = a * b; } else { result = 1.0 - (1.0 - a) * (1.0 - b); } } }
The hard light blend is a bit more complicated than the previous two. First, the gray level of the pixel in the foreground image is calculated by averaging the color channels. Then, an if statement is used to select either a multiply or a screen blend operation.
 
Again, this example uses the same ActionScript code, but it loads and applies a different shader.
 
 
Perlin grain
Now for something that is a bit more difficult to achieve with the built-in blends. The next filter uses a noise texture and sin() functions to generate a wood grain or marbling effect. The effect depends on the characteristics of the noise texture and generally looks best with Perlin-type noise (see Figure 7).
 
LayoutImage
Figure 7. A hard light blend is a combination of the multiply and screen blends.
 
The shader works by sampling the pixels values from the noise image. Instead of using the noise pixels directly in the image, the shader feeds the noise value into a series of sin() functions. The background is multiplied by the result. A turbulence parameter is used to control the curviness of the resulting effect:
 
<languageVersion: 1.0;> kernel GrainBlend < namespace : "com.adobe.example"; vendor : "Adobe Systems Inc."; version : 1; description : "Creates a wood grain or marbling effect"; > { input image4 background; input image4 noise; output pixel4 dst; parameter float turbulence < maxValue : 500.0; minValue : 0.0; defaultValue : 150.0; >; void evaluatePixel() { pixel4 a = sampleNearest(background, outCoord()); pixel4 b = sampleNearest(noise, outCoord()); float alpha = a.a; //save the original alpha if( (b.a > 0.0) && (a.a > 0.0)){ float seed = outCoord().x + (((b.r + b.g + b.b)/3.0) * turbulence); float grain = (0.7 * sin(seed) + 0.3 * sin(2.0 * seed + 0.3) + 0.2 * sin(3.0 * seed + 0.2)); dst = sampleNearest(background, outCoord()) * (grain + 0.5); dst.a = alpha; //restore the original alpha } else { //Just copy the background pixel when outside the area of the noise image dst = sampleNearest(background, outCoord()); } } }
The ActionScript code used for this example loads and applies the shader in the same way as the previous examples. In addition, the example creates a slider to control the turbulence parameter:
 
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" height="297" width="662" applicationComplete="init()" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]"> <mx:Script> <![CDATA[ import mx.events.SliderEvent; //Embed the Pixel Bender kernel in the output SWF [Embed(source="kernels/grainblend.pbj", mimeType="application/octet-stream")] private var GrainBlendKernel:Class; //Create the Shader object private var shader:Shader = new Shader( new GrainBlendKernel() ); private function init():void { //Set the slider values based on the parameter metadata turbulence.minimum = shader.data.turbulence.minValue; turbulence.maximum = shader.data.turbulence.maxValue; turbulence.value = shader.data.turbulence.defaultValue; turbulence.liveDragging = true; turbulence.addEventListener( SliderEvent.CHANGE, updateFilter ); //Apply the blend noise.blendShader = shader; } private function updateFilter( event:SliderEvent ):void { shader.data.turbulence.value = [turbulence.value]; noise.blendMode = BlendMode.NORMAL; noise.blendShader = shader; } ]]> </mx:Script> <mx:VBox height="100%" width="100%"> <mx:HBox width="100%" height="100%" horizontalAlign="center" verticalAlign="top"> <mx:VBox> <mx:Canvas width="195" height="194" backgroundColor="#663300"/> <mx:Label text="Background" textAlign="center" width="196"/> </mx:VBox> <mx:VBox> <mx:Image source="images/noise.jpg" width="195" scaleContent="false" autoLoad="true" height="194"/> <mx:Label text="Perlin noise" width="196" textAlign="center"/> </mx:VBox> <mx:VBox> <mx:Canvas width="195" height="194" backgroundColor="#663300"> <mx:Image source="images/noise.jpg" id="noise" scaleContent="false" autoLoad="true" width="195" height="194"/> </mx:Canvas> <mx:Label text="Grain blend" width="196" textAlign="center"/> </mx:VBox> </mx:HBox> <mx:HBox width="100%" horizontalAlign="center" verticalAlign="middle"> <mx:Label text="Turbulence"/> <mx:HSlider id="turbulence" width="100%"/> </mx:HBox> </mx:VBox> </mx:Application>
The example uses a slider to control the shader turbulence parameter. The minimum, maximum and starting values of the slider are set according to the parameter metadata. Then, the updateFilter() method is used to change the parameter value whenever a change event is dispatched by the Slider object. Because a shader object is cloned when you set the blendShader property of a display object, you cannot simply change the parameter value of the original Shader object. You must also reassign the updated Shader object to the blendShader property.
 
For simplicity, this example uses a bitmap for the noise texture. You can also use the perlinNoise() function of the BitmapData class to create a suitable texture.
 

 
Using filters

Shaders used as filters are applied to a single image. In addition to creating a Shader object, as we did for blends, you must also create a ShaderFilter object, passing in the Shader containing the kernel:
 
var shader:Shader = new Shader( loadedBytes ); var shaderFilter:ShaderFilter = new ShaderFilter( shader );
The ShaderFilter object "wraps" the shader and allows you to use the shader like a built-in filter, by adding it to the filters array of a display object:
 
displayObject.filters = [ shaderFilter ];
The object to which a shader is applied is automatically set as the first input of the kernel. If a filter kernel takes additional images as inputs, these must be set before the filter is assigned to a display object.
 
We've already looked at a simple filter, ChannelScrambler, so let's go straight to a more complex example, the Gaussian blur.
 
 
Gaussian blur
A Gaussian blur is an example of a convolution filter. (Convolution is a fancy word for a filter that computes a weighted average of nearby pixels.) Although the Flash Player API includes a built-in class for creating convolution filters of arbitrary size, programming a Gaussian blur in Pixel Bender is a good exercise that demonstrates several important aspects of Pixel Bender (see Figure 8).
 
LayoutImage

 
General-purpose convolution filters aren't easy to achieve in Pixel Bender because the Flash Player and AIR runtimes do not support loops in kernel code. So, instead of using a for loop, we have to write an individual program statement to sample each pixel in the neighborhood. This example kernel creates a Gaussian blur that can have a sampling radius between 1 and 6 (corresponding to convolution matrices ranging in size between 3 × 3 to 13 × 13).
 
The filter takes advantage of the fact that a Gaussian blur is separable, which simply means that you can perform the operation in two passes. One pass blurs the image horizontally, and the other pass blurs the image vertically. This saves several calculations, since for each final pixel, only about 4 times the radius pixels have to be sampled, and weighted and averaged, rather than 2 times the radius2 pixels. For example, at the largest radius supported by this filter, 26 input pixels are sampled for each final output pixel in both the vertical and horizontal passes combined. If the filter computed the blur in a single pass, 169 input pixels would have to be sampled for each output pixel. The visual and mathematical results are identical.
 
To overcome the lack of a for loop, the kernel treats each integer radius value separately. For each allowed radius value, the two pixels located at that distance to either side of the current pixel are sampled. The Gaussian weights are the same for both pixels, so they are added together. Once all the necessary pixels are sampled, the weight and scale factors are applied.
 
The following kernel code is used for the horizontal pass (a similar kernel is used for the vertical pass):
 
<languageVersion: 1.0;> kernel HorizontalGaussianBlur < namespace : "com.adobe.example"; vendor : "Adobe Systems Inc."; version : 1; description : "The horizontal convolution of a Gaussian blur"; > { input image4 src; output float4 result; parameter int radius < minValue : 1; maxValue : 6; defaultValue : 6; >; void evaluatePixel() { pixel4 center, band1, band2, band3, band4, band5, band6; float2 pos = outCoord(); //Sample image in bands if( radius > 5 ) { band6 = sampleNearest(src, float2(pos.x - 6.0, pos.y)) + sampleNearest(src, float2(pos.x + 6.0, pos.y)); } if( radius > 4 ) { band5 = sampleNearest(src, float2(pos.x - 5.0, pos.y)) + sampleNearest(src, float2(pos.x + 5.0, pos.y)); } if( radius > 3 ) { band4 = sampleNearest(src, float2(pos.x - 4.0, pos.y)) + sampleNearest(src, float2(pos.x + 4.0, pos.y)); } if( radius > 2 ) { band3 = sampleNearest(src, float2(pos.x - 3.0, pos.y)) + sampleNearest(src, float2(pos.x + 3.0, pos.y)); } if( radius > 1 ) { band2 = sampleNearest(src, float2(pos.x - 2.0, pos.y)) + sampleNearest(src, float2(pos.x + 2.0, pos.y)); } band1 = sampleNearest(src, float2(pos.x - 1.0, pos.y)) + sampleNearest(src, float2(pos.x + 1.0, pos.y)); center = sampleNearest(src, pos); //Apply weights and compute resulting pixel if( radius == 6 ) { result = (band6 + (band5 * 12.0) + (band4 * 66.0) + (band3 * 220.0) + (band2 * 495.0) + (band1 * 792.0) + (center * 924.0))/4096.0; } if( radius == 5 ) { result = (band5 + (band4 * 10.0) + (band3 * 45.0) + (band2 * 120.0) + (band1 * 210.0) + (center * 252.0))/1024.0; } if( radius == 4 ) { result = (band4 + (band3 * 8.0) + (band2 * 28.0) + (band1 * 56.0) + (center * 70.0))/256.0; } if( radius == 3 ) { result = (band3 + (band2 * 6.0) + (band1 * 15.0) + (center * 20.0))/64.0; } if( radius == 2 ) { result = (band2 + (band1 * 4.0) + (center * 6.0))/16.0; } if( radius == 1 ) { result = (band1 + (center * 2.0))/4.0; } } }
Both kernels must be applied as filters for the complete effect. It does not matter which order the filters are applied in. The ActionScript code shown below uses a slider to control the radius parameter:
 
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" width="334" height="287" applicationComplete="init()" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]" viewSourceURL="srcview/index.html"> <mx:Script> <![CDATA[ import mx.events.SliderEvent; //Embed the Pixel Bender kernel in the output SWF [Embed(source="kernels/verticalgaussianblur.pbj", mimeType="application/octet-stream")] private var VerticalBlurKernel:Class; [Embed(source="kernels/horizontalgaussianblur.pbj", mimeType="application/octet-stream")] private var HorizontalBlurKernel:Class; //Create the shaders private var vBlurShader:Shader = new Shader( new VerticalBlurKernel() ); private var hBlurShader:Shader = new Shader( new HorizontalBlurKernel() ); //Create the filters private var hBlurFilter:ShaderFilter = new ShaderFilter( hBlurShader ); private var vBlurFilter:ShaderFilter = new ShaderFilter( vBlurShader ); private function init():void { //Initialize the slider using the radius parameter metadata radiusSlider.value = hBlurShader.data.radius.value; radiusSlider.minimum = hBlurShader.data.radius.minValue; radiusSlider.maximum = hBlurShader.data.radius.maxValue; radiusSlider.addEventListener( SliderEvent.CHANGE, updateFilters ); //Apply the filters camellia.filters = [ hBlurFilter, vBlurFilter ]; } //Reapply the filters when the slider is changed private function updateFilters( event:SliderEvent ):void { hBlurShader.data.radius.value = vBlurShader.data.radius.value = [radiusSlider.value]; camellia.filters = [ hBlurFilter, vBlurFilter ]; } ]]> </mx:Script> <mx:VBox height="100%" width="285" horizontalAlign="center"> <mx:Image id="camellia" source="images/camellia.jpg" scaleContent="false" autoLoad="true"/> <mx:HBox width="100%" verticalAlign="middle" horizontalAlign="center"> <mx:Label text="Gaussian blur"/> <mx:HSlider id="radiusSlider" liveDragging="true" snapInterval="1"/> </mx:HBox> </mx:VBox> </mx:Application>
The example sets the radius of each filter to the same value. As with blends, the shader filters must be reassigned to the display object after changing parameter values.
 

 
Using fills

To use a kernel as an area fill, create a Shader object containing the kernel bytecode and pass that to the beginShaderFill() function of the display object graphics property when drawing the object. Like bitmap fills, shader fills are registered to the origin of the display object. This registration can be adjusted by using a translation matrix.
 
Shaders used as area or line fills are not automatically assigned an input image. (If you need an image as part of the fill algorithm, you must explicitly assign an image to the input before calling the beginShaderFill() method.)
 
 
Checker fill
The next fill example creates a checker pattern. The size and color of the checker squares are controlled by kernel parameters (see Figure 9).
 
LayoutImage
Figure 9. The size and color of the checker squares are controlled by kernel parameters.
 
The checker algorithm uses the modulo function to compare the current pixel position to multiples of the checker size:
 
float vertical = mod(position.x, checkerSize * 2.0);
This modulo function returns the remainder produced by dividing the x coordinate by checkerSize multiplied by 2. If, for example, checkerSize is equal to 10, then we get the pattern 0-19, 0-19, 0-19,... as x increases across the image. So whenever the result is less than the checker size, the kernel draws color A, otherwise it draws color B. That creates stripes. To produce the checker pattern, we have to apply the technique in both the horizontal and vertical directions, like this:
 
float vertical = mod(position.x, checkerSize * 2.0); float horizontal = mod(position.y, checkerSize * 2.0);
The remaining question is how to combine these results. The logical XOR operator (^^), which only returns true when just one of the inputs is true, is perfect here:
 
( vertical < checkerSize ) ^^ ( horizontal < checkerSize )
The following example displays the full kernel code:
 
<languageVersion : 1.0;> kernel CheckerFill < namespace : "com.adobe.example"; vendor : "Adobe Systems Inc."; version : 1; description : "A checkered field generator"; > { output pixel4 dst; parameter float checkerSize < defaultValue : 10.0; minValue : 1.0; maxValue : 75.0; >; parameter pixel4 colorA < defaultValue : pixel4(0.0, 1.0, 1.0, 1.0); >; parameter pixel4 colorB < defaultValue : pixel4( 0.0, 0.0, 0.0, 1.0 ); >; void evaluatePixel() { float2 position = outCoord(); float vertical = mod(position.x, checkerSize * 2.0); float horizontal = mod(position.y, checkerSize * 2.0); dst = (( vertical < checkerSize ) ^^ ( horizontal < checkerSize )) ? colorA : colorB; } }
The ActionScript code used for this example is slightly more complex than the previous examples, both because more parameters are used for the kernel and because the parameters themselves are more complex.
 
In the example, the init() function is called when at the applicationComplete event. This function creates the Shader object and uses the metadata of the kernel parameters to set up the initial values for the controls. It then calls the drawShape() function, which draws a circle using a shader fill.
 
To update the fill, the drawShape() function is called again whenever a change event is dispatched by one of the controls. The function sets the kernel parameters based on the current control values, clears the current graphics, and redraws the shape:
 
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" height="273" width="336" applicationComplete="init()" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]"> <mx:Script> <![CDATA[ import mx.core.UIComponent; import mx.events.ColorPickerEvent; import mx.events.SliderEvent; private var filledShape:UIComponent = new UIComponent(); private var areaShader:Shader; //Embed the Pixel Bender kernel in the output SWF [Embed(source="kernels/checkerfill.pbj", mimeType="application/octet-stream")] private var CheckerFillKernel:Class; private function init():void { //Create the Shader object using the embedded kernel areaShader = new Shader( new CheckerFillKernel() ); //Set controls using shader parameter metadata checkerSize.minimum = areaShader.data.checkerSize.minValue; checkerSize.maximum = areaShader.data.checkerSize.maxValue; checkerSize.value = areaShader.data.checkerSize.value; checkerSize.liveDragging = true; checkerSize.addEventListener( SliderEvent.CHANGE, updateFilters ); colorA.selectedColor = vectorToColor( areaShader.data.colorA.value ); colorA.addEventListener( ColorPickerEvent.CHANGE, updateFilters ); colorB.selectedColor = vectorToColor( areaShader.data.colorB.value ); colorB.addEventListener( ColorPickerEvent.CHANGE, updateFilters ); drawShape(); shapeHolder.addChild( filledShape ); } private function updateFilters( event:Event ):void { areaShader.data.checkerSize.value = [ checkerSize.value ]; areaShader.data.colorA.value = colorToVector( colorA.selectedColor | 0xff000000 ); areaShader.data.colorB.value = colorToVector( colorB.selectedColor | 0xff000000 ); drawShape(); } private function drawShape():void { with( filledShape.graphics ) { clear(); beginShaderFill( areaShader ); drawCircle( 125, 75, 75 ); endFill(); } } private function vectorToColor( pixelChannels:Array ):uint { return (pixelChannels[3] * 0xff << 24) | (pixelChannels[0] * 0xff << 16) | (pixelChannels[1] * 0xff << 8) | pixelChannels[2] * 0xff; } private function colorToVector( color:uint ):Array { var result:Array = new Array(4); result[3] = ((color >> 24) & 0×000000ff)/0xff; result[0] = ((color >> 16) & 0×000000ff)/0xff; result[1] = ((color >> 8) & 0×000000ff)/0xff; result[2] = (color & 0×000000ff)/0xff; return result; } ]]> </mx:Script> <mx:VBox height="100%"> <mx:HBox id="shapeHolder" width="100%" height="160"> </mx:HBox> <mx:HBox width="100%"> <mx:Label text="Square size"/> <mx:HSlider id="checkerSize"/> </mx:HBox> <mx:HBox width="100%"> <mx:Label text="Color A" width="33%"/> <mx:ColorPicker id="colorA"/> <mx:Spacer width="33%"/> <mx:Label text="Color B" width="33%"/> <mx:ColorPicker id="colorB"/> </mx:HBox> </mx:VBox> </mx:Application>
In the earlier examples we incorporated a slider to set the kernel parameters. The color picker controls used in this example are not quite as straightforward. Parameter values are accessed in ActionScript as an array. In the case of a scalar parameter, such as checkerSize, the array holds a single value. For vector types such as colors, the array contains an element for each element of the vector. Since the colorA and colorB parameters are of type pixel4, there are four values in the array, one for each channel in the pixel. A color in ActionScript, on the other hand, is represented as a single 32-bit uint value containing all the channel information. In addition, the alpha channel is the first channel in a uint color, but it is the last channel in the Shader object parameter array. The example functions, vectorToColor() and colorToVector(), translate the colors between the two forms so that we can transfer color selections between the ColorPicker objects and the shader parameters.
 
The vectorToColor() function works by multiplying each color channel returned by the Pixel Bender kernel by 256 (hexadecimal value 0xff). The resulting value is then shifted to the corresponding argb position within the 32-bit uint value using the bitwise left shift operator (<<). Finally, the four channels are combined into a single uint with the bitwise OR operator ( | ) and returned.
 
The colorToVector() function performs the inverse operation. For each channel, the function does a bitwise right shift operation (>>), masks out the bits that belong to any other channel with the bitwise AND operator (&) and divides the result by 256 (0xff) to scale the value between 0 and 1 (decimal). The results are assigned to the appropriate element of an array, in the order expected by Pixel Bender, and then returned.