Skip to content

Instantly share code, notes, and snippets.

@PrimaryFeather
Created March 12, 2015 12:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PrimaryFeather/98d34282a112a11456b9 to your computer and use it in GitHub Desktop.
Save PrimaryFeather/98d34282a112a11456b9 to your computer and use it in GitHub Desktop.
New implementation of Starling's blur filter, achieving higher blur factors with much fewer draw calls.
// =================================================================================================
//
// Starling Framework
// Copyright 2011-2014 Gamua. All Rights Reserved.
//
// This program is free software. You can redistribute and/or modify it
// in accordance with the terms of the accompanying license agreement.
//
// =================================================================================================
package utils
{
import starling.filters.*;
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import flash.display3D.Program3D;
import starling.core.Starling;
import starling.textures.Texture;
import starling.utils.Color;
/** The BlurFilter applies a Gaussian blur to an object. The strength of the blur can be
* set for x- and y-axis separately (always relative to the stage).
* A blur filter can also be set up as a drop shadow or glow filter. Use the respective
* static methods to create such a filter.
*/
public class FastBlurFilter extends FragmentFilter
{
private static const NORMAL_PROGRAM_NAME:String = "BF_n";
private static const TINTED_PROGRAM_NAME:String = "BF_t";
private var mNormalProgram:Program3D;
private var mTintedProgram:Program3D;
private var mOffsets:Vector.<Number> = new <Number>[0, 0, 0, 0];
private var mWeights:Vector.<Number> = new <Number>[0, 0, 0, 0];
private var mColor:Vector.<Number> = new <Number>[1, 1, 1, 1];
private var mPassData:Array;
private var mBlurX:Number;
private var mBlurY:Number;
private var mUniformColor:Boolean;
/** helper object */
private var sTmpWeights:Vector.<Number> = new Vector.<Number>(5, true);
/** Create a new BlurFilter. For each blur direction, the number of required passes is
* <code>Math.ceil(blur)</code>.
*
* <ul><li>blur = 0.5: 1 pass</li>
* <li>blur = 1.0: 1 pass</li>
* <li>blur = 1.5: 2 passes</li>
* <li>blur = 2.0: 2 passes</li>
* <li>etc.</li>
* </ul>
*
* <p>Instead of raising the number of passes, you should consider lowering the resolution.
* A lower resolution will result in a blurrier image, while reducing the rendering
* cost.</p>
*/
public function FastBlurFilter(blurX:Number=1, blurY:Number=1, resolution:Number=1)
{
super(1, resolution);
mBlurX = blurX;
mBlurY = blurY;
updateSettings();
}
/** Creates a blur filter that is set up for a drop shadow effect. */
public static function createDropShadow(distance:Number=4.0, angle:Number=0.785,
color:uint=0x0, alpha:Number=0.5, blur:Number=1.0,
resolution:Number=0.5):BlurFilter
{
var dropShadow:BlurFilter = new BlurFilter(blur, blur, resolution);
dropShadow.offsetX = Math.cos(angle) * distance;
dropShadow.offsetY = Math.sin(angle) * distance;
dropShadow.mode = FragmentFilterMode.BELOW;
dropShadow.setUniformColor(true, color, alpha);
return dropShadow;
}
/** Creates a blur filter that is set up for a glow effect. */
public static function createGlow(color:uint=0xffff00, alpha:Number=1.0, blur:Number=1.0,
resolution:Number=0.5):BlurFilter
{
var glow:BlurFilter = new BlurFilter(blur, blur, resolution);
glow.mode = FragmentFilterMode.BELOW;
glow.setUniformColor(true, color, alpha);
return glow;
}
/** @private */
protected override function createPrograms():void
{
mNormalProgram = createProgram(false);
mTintedProgram = createProgram(true);
}
private function createProgram(tinted:Boolean):Program3D
{
var programName:String = tinted ? TINTED_PROGRAM_NAME : NORMAL_PROGRAM_NAME;
var target:Starling = Starling.current;
if (target.hasProgram(programName))
return target.getProgram(programName);
// vc0-3 - mvp matrix
// vc4 - kernel offset
// va0 - position
// va1 - texture coords
var vertexShader:String =
"m44 op, va0, vc0 \n" + // 4x4 matrix transform to output space
"mov v0, va1 \n" + // pos: 0 |
"sub v1, va1, vc4.zwxx \n" + // pos: -2 |
"sub v2, va1, vc4.xyxx \n" + // pos: -1 | --> kernel positions
"add v3, va1, vc4.xyxx \n" + // pos: +1 | (only 1st two parts are relevant)
"add v4, va1, vc4.zwxx \n"; // pos: +2 |
// v0-v4 - kernel position
// fs0 - input texture
// fc0 - weight data
// fc1 - color (optional)
// ft0-4 - pixel color from texture
// ft5 - output color
var fragmentShader:String =
"tex ft0, v0, fs0 <2d, clamp, linear, mipnone> \n" + // read center pixel
"mul ft5, ft0, fc0.xxxx \n" + // multiply with center weight
"tex ft1, v1, fs0 <2d, clamp, linear, mipnone> \n" + // read pixel -2
"mul ft1, ft1, fc0.zzzz \n" + // multiply with weight
"add ft5, ft5, ft1 \n" + // add to output color
"tex ft2, v2, fs0 <2d, clamp, linear, mipnone> \n" + // read pixel -1
"mul ft2, ft2, fc0.yyyy \n" + // multiply with weight
"add ft5, ft5, ft2 \n" + // add to output color
"tex ft3, v3, fs0 <2d, clamp, linear, mipnone> \n" + // read pixel +1
"mul ft3, ft3, fc0.yyyy \n" + // multiply with weight
"add ft5, ft5, ft3 \n" + // add to output color
"tex ft4, v4, fs0 <2d, clamp, linear, mipnone> \n" + // read pixel +2
"mul ft4, ft4, fc0.zzzz \n"; // multiply with weight
if (tinted) fragmentShader +=
"add ft5, ft5, ft4 \n" + // add to output color
"mul ft5.xyz, fc1.xyz, ft5.www \n" + // set rgb with correct alpha
"mul oc, ft5, fc1.wwww \n"; // multiply alpha
else fragmentShader +=
"add oc, ft5, ft4 \n"; // add to output color
return target.registerProgramFromSource(programName, vertexShader, fragmentShader);
}
/** @private */
protected override function activate(pass:int, context:Context3D, texture:Texture):void
{
// already set by super class:
//
// vertex constants 0-3: mvpMatrix (3D)
// vertex attribute 0: vertex position (FLOAT_2)
// vertex attribute 1: texture coordinates (FLOAT_2)
// texture 0: input texture
updateParameters(pass, texture.nativeWidth, texture.nativeHeight);
context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 4, mOffsets);
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, mWeights);
if (mUniformColor && pass == numPasses - 1)
{
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, mColor);
context.setProgram(mTintedProgram);
}
else
{
context.setProgram(mNormalProgram);
}
}
private function updateParameters(pass:int, textureWidth:int, textureHeight:int):void
{
// algorithm described here:
// http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
//
// To run in constrained mode, we can only make 5 texture lookups in the fragment
// shader. By making use of linear texture sampling, we can produce similar output
// to what would be 9 lookups.
const passData:Object = mPassData[pass];
const pixelSize:Number = 1.0 / (passData.dir == "x" ? textureWidth : textureHeight);
const spread:Number = passData.spread * pixelSize;
const twoSigmaSq:Number = 2 * passData.sigma * passData.sigma;
const multiplier:Number = 1.0 / Math.sqrt(twoSigmaSq * Math.PI);
// get weights on the exact pixels (sTmpWeights) and calculate sums (mWeights)
for (var i:int=0; i<5; ++i)
sTmpWeights[i] = multiplier * Math.exp(-i*i / twoSigmaSq);
mWeights[0] = sTmpWeights[0];
mWeights[1] = sTmpWeights[1] + sTmpWeights[2];
mWeights[2] = sTmpWeights[3] + sTmpWeights[4];
// normalize weights so that sum equals "1.0"
var weightSum:Number = mWeights[0] + 2*mWeights[1] + 2*mWeights[2];
var invWeightSum:Number = 1.0 / weightSum;
mWeights[0] *= invWeightSum;
mWeights[1] *= invWeightSum;
mWeights[2] *= invWeightSum;
// calculate intermediate offsets
var offset1:Number = ( spread * sTmpWeights[1] + 2*spread * sTmpWeights[2]) / mWeights[1];
var offset2:Number = (3*spread * sTmpWeights[3] + 4*spread * sTmpWeights[4]) / mWeights[2];
// depending on pass, we move in x- or y-direction
if (passData.dir == "x")
{
mOffsets[0] = offset1;
mOffsets[1] = 0;
mOffsets[2] = offset2;
mOffsets[3] = 0;
}
else
{
mOffsets[0] = 0;
mOffsets[1] = offset1;
mOffsets[2] = 0;
mOffsets[3] = offset2;
}
}
private function updateSettings():void
{
if (mBlurX == 0 && mBlurY == 0) mBlurX = 0.001;
if (mPassData) mPassData.length = 0; else mPassData = [];
addPassData(mPassData, mBlurX, "x");
addPassData(mPassData, mBlurY, "y");
numPasses = mPassData.length;
marginX = (3 + Math.ceil(mBlurX)) / resolution;
marginY = (3 + Math.ceil(mBlurY)) / resolution;
}
private function addPassData(passData:Array, blur:Number, dir:String):void
{
const MAX_SIGMA:Number = 2;
var restBlur:Number = blur;
var spread:Number, sigma:Number, i:Number;
for (i = 1; restBlur > 0; i *= 2)
{
if (i <= restBlur)
{
spread = i;
sigma = MAX_SIGMA;
}
else
{
spread = restBlur;
sigma = Math.min(2, restBlur * MAX_SIGMA);
}
passData.push({ dir: dir, spread: spread, sigma: sigma });
restBlur -= i;
}
}
/** A uniform color will replace the RGB values of the input color, while the alpha
* value will be multiplied with the given factor. Pass <code>false</code> as the
* first parameter to deactivate the uniform color. */
public function setUniformColor(enable:Boolean, color:uint=0x0, alpha:Number=1.0):void
{
mColor[0] = Color.getRed(color) / 255.0;
mColor[1] = Color.getGreen(color) / 255.0;
mColor[2] = Color.getBlue(color) / 255.0;
mColor[3] = alpha;
mUniformColor = enable;
}
/** The blur factor in x-direction (stage coordinates).
* The number of required passes will be <code>Math.ceil(value)</code>. */
public function get blurX():Number { return mBlurX; }
public function set blurX(value:Number):void
{
mBlurX = value;
updateSettings();
}
/** The blur factor in y-direction (stage coordinates).
* The number of required passes will be <code>Math.ceil(value)</code>. */
public function get blurY():Number { return mBlurY; }
public function set blurY(value:Number):void
{
mBlurY = value;
updateSettings();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment