In this tutorial, I’ll cover how to use a rendering technique known as “Ping-Ponging” to achieve a colorful psychedelic post-processing effect.

Basically, it is the process of alternating between two Texture buffers, so that each passes takes one as it’s source, and the other as it’s render target. The result from the render target is rendered (or further processed!), the indexes are flipped, and the cycle repeats!

For those who can’t wait, here’s the effect in all its glory!

This area requires
Adobe FlashPlayer version 11 or above.
iOS Devices are not currently supported.

(Click away to pause the animation) 

I’ll be showing an example that extends a couple of my own classes (not shown here however). The idea behind it though is still very well exposed and should be fairly easy to setup your own Stage3D environment if you search online. I’ll also be using David Barlia’s EasyAGAL library (see here for more details) – because I definitely need a helping hand with coding shaders that are Syntax-Error free! The headaches from coding assembler language alone is enough to handle.

Here’s my Document Class for the project:

package {
	import bigp.BP3D;
	import bigp.gpu.BPIndexBuffer;
	import bigp.gpu.BPVertexBuffer;
	import bigp.utils.ByteArrayUtils;
	import bigp.utils.XMath;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.StageScaleMode;
	import flash.display3D.Context3DBlendFactor;
	import flash.display3D.Context3DTextureFormat;
	import flash.display3D.Context3DVertexBufferFormat;
	import flash.display3D.Program3D;
	import flash.display3D.textures.Texture;
	import flash.geom.ColorTransform;
	import shaders.Shader01Texture;
	import shaders.Shader02Buffer;
	 * An example of "Ping-Ponging" between two Texture Buffers to create
	 * a cool Trailing effect with a Colorful Perlin-Noise texture.
	 * @author Pierre Chamberlain
	public class Main02 extends BP3D {
		private var _scrollSpeedX:int =		0;
		private var _scrollSpeedY:int =		0;
		private var _scrollX:int =			0;
		private var _scrollY:int =			0;

		private var _noise:Texture;
		private var _noiseBmp:BitmapData;
		private var _programs:Vector.<Program3D>;
		private var _quadBuffer:BPVertexBuffer;
		private var _quadIndices:BPIndexBuffer;
		private var _texHeight:int;
		private var _texWidth:int;
		private var _textureFlip:int =		0;
		private var _texturePair:Vector.<Texture>;
		private var _textureTarget:Texture;
		private var _debugSprite:Bitmap;
		protected override function _main():void {
			//Some of these settings are inheried properties in my BP3D and Main3D class:
			backgroundColor =		0x000000;
			timerRateIdeal =		5;
			isClickedToActivate =	true;	//Shows a "Click to Run Demo" overlay
			isSelfPresented =		false;	//Indicates we're manually presenting the back-buffer
			//Create a list of Program3D for each render passes necessary:
			_programs = new Vector.<Program3D>();
			_programs[_programs.length] =	new Shader01Texture().upload(context3D);
			_programs[_programs.length] =	new Shader02Buffer().upload(context3D);
			//First create a PerlinNoise Map:
			_noiseBmp =			createPerlinNoise();
			_texWidth =			_noiseBmp.width;
			_texHeight =		_noiseBmp.height;
			//Create a texture resource to hold the Perlin Noise (in GPU):
			_noise =			createTextureFromBitmapData(_noiseBmp);
			//Create the "Ping-Pong" texture pair:
			_texturePair =		new Vector.<Texture>(2, true);
			_texturePair[0] =	context3D.createTexture(_texWidth, _texHeight, Context3DTextureFormat.BGRA, true);
			_texturePair[1] =	context3D.createTexture(_texWidth, _texHeight, Context3DTextureFormat.BGRA, true);
			//Create a QUAD (4 vertices rectangle) with Vertex & Index buffer data:
			_quadIndices =	BPIndexBuffer.fromQuads(context3D, 1);
			_quadBuffer =	BPVertexBuffer.make(context3D, 4, 4, ByteArrayUtils.times( 1, [
				-1, 1,	0, 0,
				1, 1,	1, 0,
				1, -1,	1, 1,
				-1, -1,	0, 1
			addLabel("Move Mouse Horizontaly and Vertically\n<font size=\"12\">Shader moves in direction of mouse and Blur Intensity changes from Center point.</font>", true);
			context3D.setVertexBufferAt(0, _quadBuffer.buffer, 0, Context3DVertexBufferFormat.FLOAT_4);
			var percentWidth:Number =	viewWidth / _noiseBmp.width;
			var percentHeight:Number =	viewHeight / _noiseBmp.height;
			//Set static constants:
			getVertexConst(0).setAll(0, 1, 2, .5);
			getVertexConst(1).setAll(percentWidth, percentHeight, 1/viewWidth, 1/viewHeight);
			getFragmentConst(0).setAll(0, 1, 2, .5);
			getFragmentConst(1).setAll(percentWidth, percentHeight, 1/viewWidth, 1/viewHeight);
		protected override function _calc():void {
			_scrollX += _scrollSpeedX;
			_scrollY += _scrollSpeedY;
			scrollSpeedX =	(stage.stageWidth * .5 - stage.mouseX);
			scrollSpeedY =	(stage.stageHeight * .5 - stage.mouseY);
			var zoom:Number =		.09;
			var distance:Number =	Math.sqrt(_scrollSpeedX * _scrollSpeedX + _scrollSpeedY * _scrollSpeedY);
			var distancePercent:Number =	distance / (viewAverage * .5);
			//Update constants:
			getVertexConst(2).setAll(_scrollX, _scrollY, .04, .4 + distancePercent * .40);
			getVertexConst(3).setAll(-zoom, zoom, 0, 0);
		/////////////////////////////////////// RENDERING:
		protected override function _draw():void {
			var bufferA:Texture =	_texturePair[_textureFlip];
			var bufferB:Texture =	_texturePair[1-_textureFlip];
			//arguments: Tex0, 		Tex1, 		Target, 	Program3D, 	... );
			renderPass( _noise,		bufferB,	bufferA,	_programs[0] );
			renderPass( bufferA,	null,		null,		_programs[1] );
			_textureFlip =	1 - _textureFlip;
		private function renderPass( pTex0:Texture=null, pTex1:Texture=null, pTexTarget:Texture=null, pProgram:Program3D=null, r:Number=0, g:Number=0, b:Number=0, a:Number=0, pBlendNew:String=null, pBlendOld:String=null):void {
			if (pTexTarget) {
				if(_textureTarget!=pTexTarget) {
					context3D.setRenderToTexture(pTexTarget, false, 1, 0);
					_textureTarget =	pTexTarget;
			} else {
				_textureTarget =	null;
			if (!pProgram) pProgram =	_programs[0];
			if (a>=0) clear(r,g,b,a);
			if (pBlendNew == null) pBlendNew =	Context3DBlendFactor.ONE;
			if (pBlendOld == null) pBlendOld =	Context3DBlendFactor.ZERO;
			context3D.setBlendFactors(pBlendNew, pBlendOld);
			context3D.setTextureAt(0, pTex0);
			context3D.setTextureAt(1, pTex1);
			context3D.drawTriangles(_quadIndices.buffer, 0, 2);
		/////////////////////////////////////// CREATION METHODS:
		private function createPerlinNoise():BitmapData {
			var powerWidth:int =	XMath.powerTwoNearest(viewWidth);
			var powerHeight:int =	XMath.powerTwoNearest(viewHeight);
			var bmp:BitmapData =	new BitmapData(powerWidth, powerHeight, true, 0);
			//Init ColorTransform to boost contrast (exaggerates color-patches / blobs)
			var clrM:Number =			10;		//Set the RGB Multiplier;
			var clrO:int =				-1500;	//Set the RGB Offset;
			var clr:ColorTransform =	new ColorTransform(clrM, clrM, clrM, 1, clrO, clrO, clrO);
			//Create a Multi-Color (RGB) noise:
			bmp.perlinNoise(viewWidth * .10, viewWidth * .10, 2, 64, true, true);
			bmp.colorTransform(bmp.rect, clr);
			return bmp;
		/////////////////////////////////////// GETTERS & SETTERS:
		public function get scrollSpeedX():int { return _scrollSpeedX; }
		public function set scrollSpeedX(value:int):void {
			_scrollSpeedX = value;
		public function get scrollSpeedY():int { return _scrollSpeedY; }
		public function set scrollSpeedY(value:int):void {
			_scrollSpeedY = value;

I mentioned earlier that I’m using EasyAGAL to code my shaders – true, but on top of that I wanted to have a few extra constants available at my disposal when writing my registers’ components / fields. Instead of writing them as a string (unnecessary double quotes in my opinion!), I just stuck those strings into constants in a separate class, EasyShader, that extends EasyAGAL.

Here’s the first shader responsible for our 1st rendering pass. It takes the previous buffer (the last rendered “frame” basically), scales it up slightly, fade it out, and blends it together with a UV-offsetted Perlin Noise texture by using ‘max()’. I found the results more pleasing with ‘max()’ than ‘add()’. It doesn’t bleach to white as easily.

package shaders {
	import com.barliesque.agal.EasyAGAL;
	import com.barliesque.agal.IField;
	import com.barliesque.agal.IRegister;
	import com.barliesque.agal.ISampler;
	import com.barliesque.agal.TextureFlag;
	import com.barliesque.shaders.macro.Blend;
	 * ...
	 * @author Pierre Chamberlain
	public class Shader01Texture extends EasyShader {
		public function Shader01Texture( debug:Boolean=true, assemblyDebug:Boolean=false) {
			super(debug, assemblyDebug);
		protected override function _vertexShader():void {
			var input:IRegister =		ATTRIBUTE[0];
			var temp0:IRegister =		TEMP[0];
			var temp1:IRegister =		TEMP[1];
			var const0:IRegister =		CONST[0];
			var const1:IRegister =		CONST[1];
			var const2:IRegister =		CONST[2];
			var const3:IRegister =		CONST[3];
			var var0:IRegister =		VARYING[0];
			var var1:IRegister =		VARYING[1];
			var var2:IRegister =		VARYING[2];
			mov(temp0._(BA) , const2._(WW));
			mul(temp0._(RG) , input._(ZW), const1._(RG));	//Scale UV to viewport
			mov(temp1._(XY) , const1._(ZW));	//store inverse viewport
			mul(temp1._(XY), temp1._(XY), const2._(XY));	//inverse viewport * scrollXY
			mul(temp1._(XY), temp1._(XY), const2._(ZZ));	//scrollXY(largeNumbers) * small scale
			add(temp0._(RG), temp0._(RG), temp1._(XY));	//Add offset to UVs
			mov(var0, temp0);
			//As for the Buffer UV...
			mov(var1._(XY), input._(ZW));	//Set UV (0..1)
			mov(var1._(ZW), const2._(ZW));	//Set constant values (scale, fadeout);
			mul(temp0, const3._(XY), input._(XY));
			mov(temp0._(ZW), const3._(ZW));
			mov(var2, temp0);
		protected override function _fragmentShader():void {
			var sample0:ISampler =	SAMPLER[0];
			var sample1:ISampler =	SAMPLER[1];
			var temp0:IRegister =	TEMP[0];
			var temp1:IRegister =	TEMP[1];
			var temp2:IRegister =	TEMP[2];
			var var0:IRegister =	VARYING[0];
			var var1:IRegister =	VARYING[1];
			var var2:IRegister =	VARYING[2];
			var const0:IRegister =	CONST[0];
			var flags:Array =		[
			add( temp1, var1, var2 );			//Buffer UV = original + scale(-1,1)
			tex( temp1, temp1, sample1, flags );//Buffer Texture
			mul( temp1, temp1, var1._(WWWW) );	//Buffer % alpha
			tex( temp0, var0, sample0, flags );	//Noise Texture
			max( OUTPUT, temp0, temp1 );		//Noise + Buffer


The next shader is reponsible for taking the results from our 1st pass (which was targetted to one of our Ping-Pong texture buffers), and toss it on the BackBuffer. A Fullscreen Quad (or full stage, to be specific) is necessary to draw the Texture buffer on the BackBuffer.

Note: I’m researching on the proper way to display a Render-To-Texture on the screen. By properly, I mean the correct UV mapping, the right Texture Sampling flags to use, etc. It just seems right now that the effect is a bit “blocky”. It could just be that I have to render higher-resolution perlin-noise textures, or consider using mip-maps… hmm, not sure. We’ll see! 

package shaders {
	import com.barliesque.agal.EasyAGAL;
	import com.barliesque.agal.IRegister;
	import com.barliesque.agal.TextureFlag;
	 * ...
	 * @author Pierre Chamberlain
	public class Shader02Buffer extends EasyShader {
		public function Shader02Buffer(debug:Boolean=true, assemblyDebug:Boolean=false) {
			super( debug, assemblyDebug);
		protected override function _vertexShader():void {
			var input:IRegister =		ATTRIBUTE[0];
			var temp0:IRegister =		TEMP[0];
			var temp1:IRegister =		TEMP[1];
			var const0:IRegister =		CONST[0];
			var const1:IRegister =		CONST[1];
			var const2:IRegister =		CONST[2];
			var fragment:IRegister =	VARYING[0];
			mov(fragment._(RG), input._(ZW));
			mov(fragment._(BA), const1._(XY));	//Make opaque
		protected override function _fragmentShader():void {
			var temp0:IRegister =	TEMP[0];
			var flags:Array =		[
			tex( temp0, VARYING[0], SAMPLER[0], flags);
			mov( OUTPUT, temp0 );


Special Thanks!!! :)

I wouldn’t have gone this far without the help from Ryan Speets. He’s a real great guy to have Flash discussions on Gtalk, stands way ahead of the game compared to most Flash developers, and from what I can understand he experiments with the new Flash Player features whenever he can (and is usually one of the first to report bugs to Adobe!).

His advice and the few lines of pseudocode he exchanged on is what made this tutorial possible.

Thanks Ryan!