Stage3D Post-Processing Special Effects
If you’ve ever used Instagram, you know about post-processing: full-screen effects applied just before the final image is shown to the user. With Stage3D
, we can do similar effects in real time with our 3D or 2D scenes! Today’s article will introduce you to the basic concepts behind post-processing effects and show the code for a few simple post-filters. Read on!
This is how we normally render with Stage3D
:
function renderScene(): void { context.clear(...); while (more to draw) { context.setProgram(...); context.setVertexBufferAt(...); // other state setup context.drawTriangles(...); } context.present(); }
And that’s how we’re going to keep rendering our scene. The only tweak is to do a little work before we do this. First, when the Context3D
is first set up we allocate a texture (sceneTexture
) as big as the whole screen. Then we use Context3D.setRenderToTexture
and Context3D.setRenderToBackBuffer
to control where rendering occurs:
function render(): void { // Render the scene to the texture instead context.setRenderToTexture(sceneTexture); renderScene(); // Now render the texture to the back buffer (the screen) context.setRenderToBackBuffer(); context.setTextureAt(0, sceneTexture); context.setProgram(postProcessingProgram); context.setVertexBufferAt(0, wholeScreenVertices); // other setup context.drawTriangles(...); }
Now we’re free to do whatever processing we want in the postProcessingProgram
using the scene texture in fs0
. Here is a trivial fragment shader that does no processing:
// Sample scene texture tex ft0, v0, fs0 <2d,clamp,linear> // Copy scene texture color to output mov oc, ft0
But with only a small tweak we can zero out the green and blue channels so the image is only red:
// Sample scene texture tex ft0, v0, fs0 <2d,clamp,linear> // Zero the non-red channels sub ft0.yz, ft0.yz, ft0.yz // Copy processed color to output mov oc, ft0
A red-only effect isn’t that great, but with a little imagination you can come up with some pretty cool effects. If you push a single fragment constant with some luminance coefficients then you can use that to achieve a grayscale effect:
// Sample scene texture tex ft0, v0, fs0 <2d,clamp,linear> // Apply coefficients and compute sum dp3 ft0.x, ft0, fc0 // Copy sum to all channels mov ft0.y, ft0.x mov ft0.z, ft0.x // Copy processed color to output mov oc, ft0
Based on the code in Procedurally-Generated Shape Collection, here is a test app that allows you to toggle between various post filters and disabling them altogether:
package { import com.adobe.utils.*; import flash.display.*; import flash.display3D.*; import flash.display3D.textures.*; import flash.events.*; import flash.geom.*; import flash.text.*; import flash.utils.*; public class SimplePostFilters extends Sprite { /** Number of degrees to rotate per millisecond */ private static const ROTATION_SPEED:Number = 1; /** Axis to rotate about */ private static const ROTATION_AXIS:Vector3D = new Vector3D(0, 1, 0); /** UI Padding */ private static const PAD:Number = 5; /** Distance between shapes */ private static const SHAPE_SPACING:Number = 1.5; [Embed(source="earth.jpg")] private static const TEXTURE:Class; /** Temporary matrix to avoid allocation during drawing */ private static const TEMP_DRAW_MATRIX:Matrix3D = new Matrix3D(); /** Positions for the corners of the viewport */ private static const POST_FILTER_POSITIONS:Vector.<Number> = new <Number>[ -1, 1, // TL 1, 1, // TR 1, -1, // BR -1, -1 // BL ]; /** Triangles forming a full-viewport quad */ private static const POST_FILTER_TRIS:Vector.<uint> = new <uint>[ 0, 2, 3, // bottom tri (TL, BR, BL) 0, 1, 2 // top tri (TL, TR, BR) ]; /** Constants to pass to the vertex shader for the post filter */ private static const POST_FILTER_VERTEX_CONSTANTS:Vector.<Number> = new <Number>[1, 2, 0, 0]; /** Constants to pass to the fragment shader for the grayscale post filter */ private static const GRAYSCALE_FRAGMENT_CONSTANTS:Vector.<Number> = new <Number>[0.3, 0.59, 0.11, 0]; /** Vertex shader for the red-only post filter */ private var redOnlyProgram:Program3D; /** Vertex shader for the green-only post filter */ private var greenOnlyProgram:Program3D; /** Vertex shader for the blue-only post filter */ private var blueOnlyProgram:Program3D; /** Vertex shader for the grayscale post filter */ private var grayscaleProgram:Program3D; /** 3D context to draw with */ private var context3D:Context3D; /** Shader program to draw with */ private var program:Program3D; /** Texture of all shapes */ private var texture:Texture; /** Camera viewing the 3D scene */ private var camera:Camera3D; /** Shapes to draw */ private var shapes:Vector.<Shape3D> = new Vector.<Shape3D>(); /** Current rotation of all shapes (degrees) */ private var rotationDegrees:Number = 0; /** Number of rows of shapes */ private var rows:uint = 5; /** Number of columns of shapes */ private var cols:uint = 5; /** Number of layers of shapes */ private var layers:uint = 1; /** Framerate display */ private var fps:TextField = new TextField(); /** Last time the framerate display was updated */ private var lastFPSUpdateTime:uint; /** Time when the last frame happened */ private var lastFrameTime:uint; /** Number of frames since the framerate display was updated */ private var frameCount:uint; /** 3D rendering driver display */ private var driver:TextField = new TextField(); /** Simulation statistics display */ private var stats:TextField = new TextField(); /** Name of the filter to use */ private var filterName:String = "No Filter"; /** Texture the scene is rendered to */ private var sceneTexture:Texture; /** Vertex buffer for the full-screen quad to render post-filters with */ private var postFilterVertexBuffer:VertexBuffer3D; /** Index buffer for the full-screen quad to render post-filters with */ private var postFilterIndexBuffer:IndexBuffer3D; /** * Entry point */ public function SimplePostFilters() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.frameRate = 60; var stage3D:Stage3D = stage.stage3Ds[0]; stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated); stage3D.requestContext3D(); } protected function onContextCreated(ev:Event): void { // Setup context var stage3D:Stage3D = stage.stage3Ds[0]; stage3D.removeEventListener(Event.CONTEXT3D_CREATE, onContextCreated); context3D = stage3D.context3D; context3D.configureBackBuffer( stage.stageWidth, stage.stageHeight, 0, true ); context3D.enableErrorChecking = true; // Setup camera camera = new Camera3D( 0.1, // near 100, // far stage.stageWidth / stage.stageHeight, // aspect ratio 40*(Math.PI/180), // vFOV 2, 3, 5, // position 2, 3, 0, // target 0, 1, 0 // up dir ); // Setup UI fps.background = true; fps.backgroundColor = 0xffffffff; fps.autoSize = TextFieldAutoSize.LEFT; fps.text = "Getting FPS..."; addChild(fps); driver.background = true; driver.backgroundColor = 0xffffffff; driver.text = "Driver: " + context3D.driverInfo; driver.autoSize = TextFieldAutoSize.LEFT; driver.y = fps.height; addChild(driver); stats.background = true; stats.backgroundColor = 0xffffffff; stats.text = "Getting stats..."; stats.autoSize = TextFieldAutoSize.LEFT; stats.y = driver.y + driver.height; addChild(stats); makeButtons("No Filter", "Red Only", "Green Only", "Blue Only", "Grayscale"); var assembler:AGALMiniAssembler = new AGALMiniAssembler(); // Vertex shader var vertSource:String = "m44 op, va0, vc0\nmov v0, va1\n"; assembler.assemble(Context3DProgramType.VERTEX, vertSource); var vertexShaderAGAL:ByteArray = assembler.agalcode; // Fragment shader var fragSource:String = "tex oc, v0, fs0 <2d,linear,mipnone>"; assembler.assemble(Context3DProgramType.FRAGMENT, fragSource); var fragmentShaderAGAL:ByteArray = assembler.agalcode; // Shader program program = context3D.createProgram(); program.upload(vertexShaderAGAL, fragmentShaderAGAL); // Setup shapes texture var bmd:BitmapData = (new TEXTURE() as Bitmap).bitmapData; texture = context3D.createTexture( bmd.width, bmd.height, Context3DTextureFormat.BGRA, true ); texture.uploadFromBitmapData(bmd); // Post filter vertex shader vertSource = // Pass position through unchanged. It's already in clip space. "mov op, va0\n" + // Position = (position+1)/2 // Transforms [-1,1] to [0,1] "add vt0, vc0.xxxx, va0\n" + "div vt0, vt0, vc0.yyyy\n" + "sub vt0.y, vc0.x, vt0.y\n" + "mov v0, vt0\n"; assembler.assemble(Context3DProgramType.VERTEX, vertSource); vertexShaderAGAL = assembler.agalcode; // Red-only post filter fragment shader fragSource = // Sample scene texture "tex ft0, v0, fs0 <2d,clamp,linear>\n" + // Zero the non-red channels "sub ft0.yz, ft0.yz, ft0.yz\n" + "mov oc, ft0\n"; assembler.assemble(Context3DProgramType.FRAGMENT, fragSource); fragmentShaderAGAL = assembler.agalcode; // Red-only post filter shader program redOnlyProgram = context3D.createProgram(); redOnlyProgram.upload(vertexShaderAGAL, fragmentShaderAGAL); // Green-only post filter fragment shader fragSource = // Sample scene texture "tex ft0, v0, fs0 <2d,clamp,linear>\n" + // Zero the non-green channels "sub ft0.xz, ft0.xz, ft0.xz\n" + "mov oc, ft0\n"; assembler.assemble(Context3DProgramType.FRAGMENT, fragSource); fragmentShaderAGAL = assembler.agalcode; // Green-only post filter shader program greenOnlyProgram = context3D.createProgram(); greenOnlyProgram.upload(vertexShaderAGAL, fragmentShaderAGAL); // Blue-only post filter fragment shader fragSource = // Sample scene texture "tex ft0, v0, fs0 <2d,clamp,linear>\n" + // Zero the non-blue channels "sub ft0.xy, ft0.xy, ft0.xy\n" + "mov oc, ft0\n"; assembler.assemble(Context3DProgramType.FRAGMENT, fragSource); fragmentShaderAGAL = assembler.agalcode; // Blue-only post filter shader program blueOnlyProgram = context3D.createProgram(); blueOnlyProgram.upload(vertexShaderAGAL, fragmentShaderAGAL); // Grayscale post filter fragment shader fragSource = // Sample scene texture "tex ft0, v0, fs0 <2d,clamp,linear>\n" + // Apply coefficients and compute sum "dp3 ft0.x, ft0, fc0\n" + // Copy sum to all channels "mov ft0.y, ft0.x\n" + "mov ft0.z, ft0.x\n" + "mov oc, ft0\n"; assembler.assemble(Context3DProgramType.FRAGMENT, fragSource); fragmentShaderAGAL = assembler.agalcode; // Grayscale post filter shader program grayscaleProgram = context3D.createProgram(); grayscaleProgram.upload(vertexShaderAGAL, fragmentShaderAGAL); // Setup scene texture sceneTexture = context3D.createTexture( nextPowerOfTwo(stage.stageWidth), nextPowerOfTwo(stage.stageHeight), Context3DTextureFormat.BGRA, true ); // Post filter full-screen quad vertex and index buffers postFilterVertexBuffer = context3D.createVertexBuffer(4, 2); postFilterVertexBuffer.uploadFromVector(POST_FILTER_POSITIONS, 0, 4); postFilterIndexBuffer = context3D.createIndexBuffer(6); postFilterIndexBuffer.uploadFromVector(POST_FILTER_TRIS, 0, 6); makeShapes(); // Start the simulation addEventListener(Event.ENTER_FRAME, onEnterFrame); } /** * Get the next-highest power of two * @param v Value to get the next-highest power of two from * @return The next-highest power of two from the given value */ public static function nextPowerOfTwo(v:uint): uint { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } private function makeShapes(): void { for each (var shape:Shape3D in shapes) { shape.dispose(); } shapes.length = 0; for (var row:int = 0; row < rows; ++row) { for (var col:int = 0; col < cols; ++col) { for (var layer:int = 0; layer < layers; ++layer) { var posX:Number = col*SHAPE_SPACING; var posY:Number = row*SHAPE_SPACING; var posZ:Number = -layer*SHAPE_SPACING; var rand:Number = Math.random(); if (rand < 1/6) { shape = new Cylinder3D(20, context3D, posX, posY, posZ); } else if (rand < 2/6) { shape = new Sphere3D(20, 20, context3D, posX, posY, posZ); } else if (rand < 3/6) { shape = new Cube3D(context3D, posX, posY, posZ); } else if (rand < 4/6) { shape = new Pyramid3D(context3D, posX, posY, posZ); } else if (rand < 5/6) { shape = new Circle3D(20, context3D, posX, posY, posZ); } else { shape = new Quad3D(context3D, posX, posY, posZ); } shapes.push(shape); } } } var numShapes:uint = rows*cols*layers; stats.text = "Shapes: (rows=" + rows + ", cols=" + cols + ", layers=" + layers + ", total=" + numShapes + ")"; } private function makeButtons(...labels): Number { var curX:Number = PAD; var curY:Number = stage.stageHeight - PAD; for each (var label:String in labels) { if (label == null) { curX = PAD; curY -= button.height + PAD; continue; } var tf:TextField = new TextField(); tf.mouseEnabled = false; tf.selectable = false; tf.defaultTextFormat = new TextFormat("_sans"); tf.autoSize = TextFieldAutoSize.LEFT; tf.text = label; tf.name = "lbl"; var button:Sprite = new Sprite(); button.buttonMode = true; button.graphics.beginFill(0xF5F5F5); button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD); button.graphics.endFill(); button.graphics.lineStyle(1); button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD); button.addChild(tf); button.addEventListener(MouseEvent.CLICK, onButton); if (curX + button.width > stage.stageWidth - PAD) { curX = PAD; curY -= button.height + PAD; } button.x = curX; button.y = curY - button.height; addChild(button); curX += button.width + PAD; } return curY - button.height; } private function onButton(ev:MouseEvent): void { filterName = TextField(Sprite(ev.target).getChildByName("lbl")).text; } private function onEnterFrame(ev:Event): void { switch (filterName) { case "No Filter": renderShapes(); break; case "Red Only": renderWithPostFilter(redOnlyProgram, null); break; case "Green Only": context3D.setRenderToBackBuffer(); renderWithPostFilter(greenOnlyProgram, null); break; case "Blue Only": renderWithPostFilter(blueOnlyProgram, null); break; case "Grayscale": renderWithPostFilter(grayscaleProgram, GRAYSCALE_FRAGMENT_CONSTANTS); break; } context3D.present(); rotationDegrees += ROTATION_SPEED; // Update stat displays frameCount++; var now:int = getTimer(); var elapsed:int = now - lastFPSUpdateTime; if (elapsed > 1000) { var framerateValue:Number = 1000 / (elapsed / frameCount); fps.text = "FPS: " + framerateValue.toFixed(1); lastFPSUpdateTime = now; frameCount = 0; } lastFrameTime = now; } private function renderShapes(): void { // Set up rendering context3D.setProgram(program); context3D.setTextureAt(0, texture); context3D.clear(0.5, 0.5, 0.5); // Draw shapes var worldToClip:Matrix3D = camera.worldToClipMatrix; var drawMatrix:Matrix3D = TEMP_DRAW_MATRIX; for each (var shape:Shape3D in shapes) { context3D.setVertexBufferAt(0, shape.positions, 0, Context3DVertexBufferFormat.FLOAT_3); context3D.setVertexBufferAt(1, shape.texCoords, 0, Context3DVertexBufferFormat.FLOAT_2); shape.modelToWorld.copyToMatrix3D(drawMatrix); drawMatrix.appendRotation(rotationDegrees, ROTATION_AXIS); drawMatrix.prepend(worldToClip); context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, drawMatrix, false); context3D.drawTriangles(shape.tris); } } private function renderWithPostFilter(program:Program3D, fragConsts:Vector.<Number>): void { // Render the scene to the scene texture context3D.setRenderToTexture(sceneTexture, true); renderShapes(); context3D.setRenderToBackBuffer(); // Render a full-screen quad with the scene texture to the actual screen context3D.setProgram(program); context3D.setTextureAt(0, sceneTexture); context3D.clear(0.5, 0.5, 0.5); context3D.setVertexBufferAt(0, postFilterVertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); context3D.setVertexBufferAt(1, null); context3D.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 0, POST_FILTER_VERTEX_CONSTANTS); if (fragConsts) { context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, fragConsts); } context3D.drawTriangles(postFilterIndexBuffer); } } }
Download the source code and test texture
Spot a bug? Have a suggestion? Want to share a post-processing effect you’ve made? Post a comment!
#1 by Henke37 on October 22nd, 2012 ·
Now to show some classical serious post processing effects. Like say, bloom, color adjustment, more bloom, fuzz, even more bloom, scanlines, that thing where you blurr everything and do additive rendering, depth based blurring and yes, bloom again.
#2 by jackson on October 22nd, 2012 ·
There are indeed many cool effects you can implement with post-processing. This article is only a primer. Perhaps I’ll cover some of the more advanced ones later.
#3 by AlexG on October 26th, 2012 ·
I made some effects using PixelBender. Looks like here are pretty similar principles based on pixel color manipulations?
#4 by sindney on October 28th, 2012 ·
Nice article. I have a question about implementing advanced post effects.
You know they need more draw calls, depth buffer and so on.
And now I need z buffer to make one. I realized that context3d’s configure backbuffer has a depth buffer switch, is there any way we can get access to it? So we don’t need to calculate zbuffer on our own.
#5 by jackson on October 28th, 2012 ·
I wish there was a way to get access to the depth buffer, but I don’t know of one in the
Stage3D
API. I think you’ll need to render distances from cameras (Z) to a screen-size texture and then use that along with the next draw, similar to the technique described in this article for filtering.