How to write and use a simple post-processing shader

Introduction

In this tutorial we will show how to create a very simple post-processing effect by defining a custom post-processing shader and using it in a simple scene. Post-processing happens after everything has been rendered to the screen. The post-processing effect is then applied to every pixel of the back buffer to compute its final color. Post-processing is very interesting because it makes it possible to create a wide variety of scene-independant effects such as motion blur, color distortion, patterns, color replacement, vignettage, HDR bloom...

In this tutorial, our effect will simply display everything in black and white. Here is the final result we will get:

In this tutorial, the scene will contain just a single cube. But the scene and the shader are completely unrelated. Indeed, post-processing shaders are completely scene independant. Therefore, one can use the shader described in this tutorial on any 3D scene.

Step 1: Initializing The Scene

We start by defining a very simple scene containing only a textured cube and an ArcBallCamera:

public class Main extends Sprite { [Embed("../assets/box.jpg")] private static const ASSET_BOX : Class; private var _viewport : Viewport = new Viewport(); private var _camera : ArcBallCamera = new ArcBallCamera(); private var _scene : Group = new Group( _camera, LoaderGroup.loadClass(ASSET_BOX)[0], CubeMesh.cubeMesh ); public function Main() { _camera.distance = 3.; stage.addChild(_viewport); stage.addEventListener(Event.ENTER_FRAME); } private function enterFrameHandler(event : Event) : void { _viewport.render(_scene); } }

Detailed step-by-step instructions to build such a scene are available in the "Display a textured cube" tutorial.

Step 2: The Post-Processing Shader

We are going to write a very simple post-processing shader using ActionScript. Our shader will simply display the geometry in black and white. To write an ActionScript post-processing shader, you must:

  • Create a new class that extends PostProcessingActionScriptShader.
  • Override the PostProcessingActionScriptShader.getFinalColor() method to define the post-processing treatment and return the final color.

The goal of the post-processing fragment shader is to modify the color of each fragment (pixel) already available in the back buffer. The color modification can be computed in a lot of different ways to handle fancy effects such as light scattering, bloom, depth of field, etc... In this case, we will just say that each pixel color is the average value of its red, green and blue components:

public class GreyscalePostProcessing extends PostProcessingActionScriptShader { override protected function getFinalColor(outputColor : SValue) : SValue { return float4( float3(divide(add(outputColor.r, outputColor.g, outputColor.b), 3.)), 1.0 ); } }

The outputColor parameter is the color of the current pixel computed by the fragment shader that was used to draw into the back buffer.

Step 3: Setup The Shader

Now that we have a proper post-processing shader, we must update our scene setup to make sure it is used properly. The post-processing effect used in a scene is set in the Viewport.postProcessingEffect property. To use our custom shader, we simply have to set this property to a new post-processing effect that will use our shader:

_viewport.postProcessingtEffect = new SinglePassPostProcessingEffect( new GreyscalePostProcessingShader() );

Conclusion

Here is the final code for our application:

public class Main extends Sprite { [Embed("../assets/box.jpg")] private static const ASSET_BOX : Class; private var _viewport : Viewport = new Viewport(); private var _camera : ArcBallCamera = new ArcBallCamera(); private var _scene : Group = new Group( _camera, LoaderGroup.loadClass(ASSET_BOX)[0], CubeMesh.cubeMesh ); public function Main() { _camera.distance = 3.; _viewport.postProcessingtEffect = new SinglePassPostProcessingEffect( new GreyscalePostProcessingShader() ); stage.addChild(_viewport); stage.addEventListener(Event.ENTER_FRAME); } private function enterFrameHandler(event : Event) : void { _viewport.render(_scene); } } public class GreyscalePostProcessing extends PostProcessingActionScriptShader { override protected function getFinalColor(outputColor : SValue) : SValue { return float4( float3(divide(add(outputColor.r, outputColor.g, outputColor.b), 3.)), 1.0 ); } }