// =================================================================================================
//
// Starling Framework
// Copyright Gamua GmbH. 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 starling.filters
{
import flash.display3D.Context3DTextureFormat;
import flash.errors.IllegalOperationError;
import flash.geom.Matrix3D;
import flash.geom.Rectangle;
import starling.core.Starling;
import starling.core.starling_internal;
import starling.display.DisplayObject;
import starling.display.Stage;
import starling.events.Event;
import starling.events.EventDispatcher;
import starling.rendering.FilterEffect;
import starling.rendering.IndexData;
import starling.rendering.Painter;
import starling.rendering.VertexData;
import starling.textures.Texture;
import starling.textures.TextureSmoothing;
import starling.utils.MatrixUtil;
import starling.utils.Padding;
import starling.utils.Pool;
import starling.utils.RectangleUtil;
/** Dispatched when the settings change in a way that requires a redraw. */
[Event(name="change", type="starling.events.Event")]
/** Dispatched every frame on filters assigned to display objects connected to the stage. */
[Event(name="enterFrame", type="starling.events.EnterFrameEvent")]
/** The FragmentFilter class is the base class for all filter effects in Starling.
* All filters must extend this class. You can attach them to any display object through the
* filter
property.
*
*
A fragment filter works in the following way:
*process
method.FilterEffect
subclass
* that processes the input via fragment and vertex shaders to achieve a certain
* effect.alwaysDrawToBackBuffer
property.All of this is set up by the basic FragmentFilter class. Concrete subclasses
* just need to override the protected method createEffect
and (optionally)
* process
. Multi-pass filters must also override numPasses
.
Typically, any properties on the filter are just forwarded to an effect instance,
* which is then used automatically by process
to render the filter pass.
* For a simple example on how to write a single-pass filter, look at the implementation of
* the ColorMatrixFilter
; for a composite filter (i.e. a filter that combines
* several others), look at the GlowFilter
.
*
Beware that a filter instance may only be used on one object at a time!
* *Animated filters
* *The process
method of a filter is only called when it's necessary, i.e.
* when the filter properties or the target display object changes. This means that you cannot
* rely on the method to be called on a regular basis, as needed when creating an animated
* filter class. Instead, you can do so by listening for an ENTER_FRAME
-event.
* It is dispatched on the filter once every frame, as long as the filter is assigned to
* a display object that is connected to the stage.
Caching
* *Per default, whenever the target display object is changed in any way (i.e. the render
* cache fails), the filter is reprocessed. However, you can manually cache the filter output
* via the method of the same name: this will let the filter redraw the current output texture,
* even if the target object changes later on. That's especially useful if you add a filter
* to an object that changes only rarely, e.g. a TextField or an Image. Keep in mind, though,
* that you have to call cache()
again in order for any changes to show up.
helper
) that
* contains the filtered output. To to do this, it configures the FilterEffect
* (provided via createEffect
) and calls its render
method.
*
* In a standard filter, only input0
will contain a texture; that's the
* object the filter was applied to, rendered into an appropriately sized texture.
* However, filters may also accept multiple textures; that's useful when you need to
* combine the output of several filters into one. For example, the DropShadowFilter
* uses a BlurFilter to create the shadow and then feeds both input and shadow texture
* into a CompositeFilter.
Never create or dispose any textures manually within this method; instead, get * new textures from the provided helper object, and pass them to the helper when you do * not need them any longer. Ownership of both input textures and returned texture * lies at the caller; only temporary textures should be put into the helper.
*/ public function process(painter:Painter, helper:IFilterHelper, input0:Texture=null, input1:Texture=null, input2:Texture=null, input3:Texture=null):Texture { var effect:FilterEffect = this.effect; var output:Texture = helper.getTexture(_resolution); var projectionMatrix:Matrix3D; var bounds:Rectangle = null; var renderTarget:Texture; if (output) // render to texture { renderTarget = output; projectionMatrix = MatrixUtil.createPerspectiveProjectionMatrix(0, 0, output.root.width / _resolution, output.root.height / _resolution, 0, 0, null, sMatrix3D); } else // render to back buffer { bounds = helper.targetBounds; renderTarget = (helper as FilterHelper).renderTarget; projectionMatrix = (helper as FilterHelper).projectionMatrix3D; effect.textureSmoothing = _textureSmoothing; } painter.state.renderTarget = renderTarget; painter.prepareToDraw(); painter.drawCount += 1; input0.setupVertexPositions(vertexData, 0, "position", bounds); input0.setupTextureCoordinates(vertexData); effect.texture = input0; effect.mvpMatrix3D = projectionMatrix; effect.uploadVertexData(vertexData); effect.uploadIndexData(indexData); effect.render(0, indexData.numTriangles); return output; } /** Creates the effect that does the actual, low-level rendering. * Must be overridden by all subclasses that do any rendering on their own (instead * of just forwarding processing to other filters). */ protected function createEffect():FilterEffect { return new FilterEffect(); } /** Caches the filter output into a texture. * *An uncached filter is rendered every frame (except if it can be rendered from the
* global render cache, which happens if the target object does not change its appearance
* or location relative to the stage). A cached filter is only rendered once; the output
* stays unchanged until you call cache
again or change the filter settings.
*
Beware: you cannot cache filters on 3D objects; if the object the filter is attached * to is a Sprite3D or has a Sprite3D as (grand-) parent, the request will be silently * ignored. However, you can cache a 2D object that has 3D children!
*/ public function cache():void { _cached = _cacheRequested = true; setRequiresRedraw(); } /** Clears the cached output of the filter. After calling this method, the filter will be * processed once per frame again. */ public function clearCache():void { _cached = _cacheRequested = false; setRequiresRedraw(); } // enter frame event /** @private */ override public function addEventListener(type:String, listener:Function):void { if (type == Event.ENTER_FRAME && _target) _target.addEventListener(Event.ENTER_FRAME, onEnterFrame); super.addEventListener(type, listener); } /** @private */ override public function removeEventListener(type:String, listener:Function):void { if (type == Event.ENTER_FRAME && _target) _target.removeEventListener(type, onEnterFrame); super.removeEventListener(type, listener); } private function onEnterFrame(event:Event):void { dispatchEvent(event); } // properties /** The effect instance returning the FilterEffect created viacreateEffect
. */
protected function get effect():FilterEffect
{
if (_effect == null) _effect = createEffect();
return _effect;
}
/** The VertexData used to process the effect. Per default, uses the format provided
* by the effect, and contains four vertices enclosing the target object. */
protected function get vertexData():VertexData
{
if (_vertexData == null) _vertexData = new VertexData(effect.vertexFormat, 4);
return _vertexData;
}
/** The IndexData used to process the effect. Per default, references a quad (two triangles)
* of four vertices. */
protected function get indexData():IndexData
{
if (_indexData == null)
{
_indexData = new IndexData(6);
_indexData.addQuad(0, 1, 2, 3);
}
return _indexData;
}
/** Call this method when any of the filter's properties changes.
* This will make sure the filter is redrawn in the next frame. */
protected function setRequiresRedraw():void
{
dispatchEventWith(Event.CHANGE);
if (_target) _target.setRequiresRedraw();
if (_cached) _cacheRequested = true;
}
/** Indicates the number of rendering passes required for this filter.
* Subclasses must override this method if the number of passes is not 1
. */
public function get numPasses():int
{
return 1;
}
/** Called when assigning a target display object.
* Override to plug in class-specific logic. */
protected function onTargetAssigned(target:DisplayObject):void
{ }
/** Padding can extend the size of the filter texture in all directions.
* That's useful when the filter "grows" the bounds of the object in any direction. */
public function get padding():Padding
{
if (_padding == null)
{
_padding = new Padding();
_padding.addEventListener(Event.CHANGE, setRequiresRedraw);
}
return _padding;
}
public function set padding(value:Padding):void
{
padding.copyFrom(value);
}
/** Indicates if the filter is cached (via the cache
method). */
public function get isCached():Boolean { return _cached; }
/** The resolution of the filter texture. "1" means stage resolution, "0.5" half the stage
* resolution. A lower resolution saves memory and execution time, but results in a lower
* output quality. Values greater than 1 are allowed; such values might make sense for a
* cached filter when it is scaled up. @default 1
*/
public function get resolution():Number { return _resolution; }
public function set resolution(value:Number):void
{
if (value != _resolution)
{
if (value > 0) _resolution = value;
else throw new ArgumentError("resolution must be > 0");
setRequiresRedraw();
}
}
/** The smoothing mode of the filter texture. @default bilinear */
public function get textureSmoothing():String { return _textureSmoothing; }
public function set textureSmoothing(value:String):void
{
if (value != _textureSmoothing)
{
_textureSmoothing = value;
if (_quad) _quad.textureSmoothing = value;
setRequiresRedraw();
}
}
/** The format of the filter texture. @default BGRA */
public function get textureFormat():String { return _textureFormat; }
public function set textureFormat(value:String):void
{
if (value != _textureFormat)
{
_textureFormat = value;
if (_helper) _helper.textureFormat = value;
setRequiresRedraw();
}
}
/** Indicates if the last filter pass is always drawn directly to the back buffer.
*
* Per default, the filter tries to automatically render in a smart way: objects that * are currently moving are rendered to the back buffer, objects that are static are * rendered into a texture first, which allows the filter to be drawn directly from the * render cache in the next frame (in case the object remains static).
* *However, this fails when filters are added to an object that does not support the * render cache, or to a container with such a child (e.g. a Sprite3D object or a masked * display object). In such a case, enable this property for maximum performance.
* * @default false */ public function get alwaysDrawToBackBuffer():Boolean { return _alwaysDrawToBackBuffer; } public function set alwaysDrawToBackBuffer(value:Boolean):void { _alwaysDrawToBackBuffer = value; } // internal methods /** @private */ starling_internal function setTarget(target:DisplayObject):void { if (target != _target) { var prevTarget:DisplayObject = _target; _target = target; if (target == null) { if (_helper) _helper.purge(); if (_effect) _effect.purgeBuffers(); if (_quad) _quad.disposeTexture(); } if (prevTarget) { prevTarget.filter = null; prevTarget.removeEventListener(Event.ENTER_FRAME, onEnterFrame); } if (target) { if (hasEventListener(Event.ENTER_FRAME)) target.addEventListener(Event.ENTER_FRAME, onEnterFrame); onTargetAssigned(target); } } } } } import flash.geom.Matrix; import flash.geom.Rectangle; import starling.display.DisplayObject; import starling.display.Mesh; import starling.rendering.IndexData; import starling.rendering.VertexData; import starling.textures.Texture; class FilterQuad extends Mesh { private static var sMatrix:Matrix = new Matrix(); public function FilterQuad(smoothing:String) { var vertexData:VertexData = new VertexData(null, 4); vertexData.numVertices = 4; var indexData:IndexData = new IndexData(6); indexData.addQuad(0, 1, 2, 3); super(vertexData, indexData); textureSmoothing = smoothing; pixelSnapping = false; } override public function dispose():void { disposeTexture(); super.dispose(); } public function disposeTexture():void { if (texture) { texture.dispose(); texture = null; } } public function moveVertices(sourceSpace:DisplayObject, targetSpace:DisplayObject):void { if (targetSpace.is3D) throw new Error("cannot move vertices into 3D space"); else if (sourceSpace != targetSpace) { targetSpace.getTransformationMatrix(sourceSpace, sMatrix).invert(); // ss could be null! vertexData.transformPoints("position", sMatrix); } } public function setBounds(bounds:Rectangle):void { var vertexData:VertexData = this.vertexData; var attrName:String = "position"; vertexData.setPoint(0, attrName, bounds.x, bounds.y); vertexData.setPoint(1, attrName, bounds.right, bounds.y); vertexData.setPoint(2, attrName, bounds.x, bounds.bottom); vertexData.setPoint(3, attrName, bounds.right, bounds.bottom); } override public function set texture(value:Texture):void { super.texture = value; if (value) value.setupTextureCoordinates(vertexData); } }