Les shader en flash

Pixel bender toolkit

Adobe permet aux développeurs avertis de créer des filtres personnalisées pour Photoshop et Flash. Sous Flash ces filtres sont utilisables en tant que Shader.

Le fichier généré par Pixel Bender est un fichier binaire et comme le langage utilisé pour générer ce code est du C++, celui-ci a l’énorme avantage d’être moins gourmand en ressource.

Exemple

Télécharger les fichiers de l’exemple

Pour ce tuto j’ai été très inspiré par le tutoriel de ActiveTuts, qui explique comment créer un effet de distorsion d’un écran CRT. Tutoriel que je vous invite à regarder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<languageVersion : 1.0;>

kernel CRTBuzz
<   namespace : "multifreid";
    vendor : "MultiFreid";
    version : 1;
>
{
    input image4 src;
    output pixel4 dst;
    //Parameter that will move the different channel
    parameter float2 deltaRed;
    parameter float2 deltaGreen;
    parameter float2 deltaBlue;

    void
    evaluatePixel()
    {
        pixel4 red, green, blue;

        red = sampleNearest(src,outCoord()+deltaRed);
        green = sampleNearest(src,outCoord()+deltaGreen);
        blue = sampleNearest(src,outCoord()+deltaBlue);

        dst = red;
        dst.g = green.g;
        dst.b = blue.b;
    }
}

La fonction evaluatePixel() est la fonction qui etre appelé pour produire le pixel final (dst) à partir de l’image source (src).

SampleNearest permet d’effectuer un échantillonnage du pixel de l’image aux coordonnées indiquées en paramètre. Ici on crée un échantillonnage pour chaque décalage passé en paramètre du shader.

Enfin on crée dst à partir de ces trois échantillonnages.

Faites vos tests puis exportez le filtre (File>Export filter for Flash Player…). Pixel Bender  crée alors un fichier .pbj qui est le code binaire de notre filtre.


Dans le code flash il nous faut crée un filtre que l’on pourra utilisé comme DropShadowFilter ou BlurFilter.

Pour cela je crée la classe RGBDistortFilter qui étend ShaderFilter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package  {
    import flash.display.Shader;
    import flash.filters.ShaderFilter;

    public class RGBDistortFilter extends ShaderFilter {
        public static var theShader:Shader;
        public function RGBDistortFilter(redX:Number=0, redY:Number=0, greenX:Number =0, greenY:Number =0, blueX:Number = 0, blueY:Number = 0) {
            // constructor code
            if(theShader)
            {

                theShader.data.deltaRed.value = [redX, redY];
                theShader.data.deltaGreen.value = [greenX, greenY];
                theShader.data.deltaBlue.value = [blueX, blueY];
                super(theShader);
                this.bottomExtension = this.leftExtension =this.rightExtension = this.topExtension = Math.ceil(Math.max(Math.abs(redX), Math.abs(redY),Math.abs(greenX),
                                                            Math.abs(greenY), Math.abs(blueX), Math.abs(blueY)));
            }
        }
    }
}

Enfin dans la classe Main, j’y fait appel comme suit :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package  {
    import flash.display.MovieClip;
    import flash.events.Event;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.display.Shader;
    import com.greensock.TweenLite;

    public class Main extends MovieClip{
        var urlLoader:URLLoader;
        var valFilter:Object;

        public function Main() {
            // constructor code
            urlLoader =new URLLoader();
            urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
            urlLoader.addEventListener(Event.COMPLETE, onComplete);
            urlLoader.load(new URLRequest("RGBDistortFilter.pbj"));

             valFilter= new Object();

            valFilter.xr=0;
            valFilter.yr=0;

            valFilter.xg=0;
            valFilter.yg=0;

            valFilter.xb=0;
            valFilter.yb=0;
        }

        function onComplete(evt:Event=null)
        {
            if(evt)
            {
                RGBDistortFilter.theShader = new Shader();
                RGBDistortFilter.theShader.byteCode = evt.currentTarget.data;
            }
            var amplitudeMax:Number = 5;
            TweenLite.to(valFilter, .2, {
                yr: (Math.random()-.5)*amplitudeMax,
                yg: (Math.random()-.5)*amplitudeMax,
                yb: (Math.random()-.5)*amplitudeMax,
                onUpdate:updateFilter,
                onComplete:onComplete});
        }

        function updateFilter()
        {
            var filter: RGBDistortFilter = new RGBDistortFilter(valFilter.xr, valFilter.yr, valFilter.xg, valFilter.yg, valFilter.xb, valFilter.yb);
            titre.filters = [filter];
        }

    }
}

J’utilise la library de tween de greensock pour faire l’animation du filtre. mais il existe de nombreuses techniques pour ça (comme le bruit de Perlin par exemple).

Séparer le code des média

Quel intérêt?

Lors d’un projet on est rarement seul à travailler dessus. Et donc il peut arriver que le ou les graphistes aient à modifier les médias pendant que le ou les développeurs avancent sur le projet.

Il arrive aussi souvent d’avoir un projet multilingue. La séparation des média et des codes peut faciliter la tâche. Sur certain projet il suffira d’externaliser les textes dans un XML et ça ira ! Mais qu’en est-il des logos ? des FX qui possède du textes ? ou même l’intégration de texte dans le décors ? Très souvent un XML ne suffit pas pour toutes ces problématiques.

Il y a d’autres projets qui nécessite de changer de style à l’occasion de Noel, Halloween, l’été ou pour n’importe quelle raison ! Il faut pouvoir changer de média sans avoir à changer de code.

Une classe pour contrôler ses médias

Pour pouvoir gérer complètement et facilement la séparation du code et des médias j’ai créé la classe SharedLibrary.

Télécharger l’exemple

package  multifred.utils
{
	import flash.display.DisplayObject;
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.display.Loader;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.net.URLRequest;
	import flash.display.LoaderInfo;
	import flash.system.ApplicationDomain;

	/**
	 * ...
	 * @author MultiFred
	 */
	public class SharedLibrary extends EventDispatcher
	{
		protected var _applicationDomain:ApplicationDomain;
		private var loader:Loader;
		/**
		 * Charge un swf et recupere son ApplicationDomain. Cette diffuse les evenement Event.COMPLETE, ProgressEvent.PROGRESS, IOErrorEvent.IO_ERROR et Event.INIT.
		 * @param	url 	URL de la bibliothèque (fichier swf)
		 */
		public function SharedLibrary(url:String)
		{
			_applicationDomain=new ApplicationDomain()
			loader = new Loader();
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
			loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, onProgress);
			loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onError);
			loader.contentLoaderInfo.addEventListener(Event.INIT, onInit);
			loader.load(new URLRequest(url));
		}

		public function get applicationDomain():ApplicationDomain {
			return _applicationDomain;
		}

		public function get percent():Number {
			var val:Number
			try
			{
				val=loader.contentLoaderInfo.bytesLoaded / loader.contentLoaderInfo.bytesTotal;
			}
			catch (err:Error)
			{
				val=0
			}
			return val;
		}
		public function getClass(className:String):Class
		{
			return _applicationDomain.getDefinition(className) as Class;
		}

		public function createClip(className:String):DisplayObject
		{
			var TheClass:Class;
			var theClip:DisplayObject;

			TheClass = getClass(className);
			theClip = new TheClass();
			return theClip;
		}

		private function onComplete(evt:Event) {
			/*
			* Chargement des classes contenus dans le fichier swf chargé
			*/
			// récupération d'une instance d'ApplicationDomain
			// sur la propriété applicationDomain du contentLoaderInfo (e.target)
			_applicationDomain = LoaderInfo(evt.target).applicationDomain;
			dispatchEvent(new Event(Event.COMPLETE));
		}

		private function onProgress(evt:ProgressEvent) {
			dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS,false, false, evt.bytesLoaded, evt.bytesTotal));
		}

		private function onError(evt:IOErrorEvent) {
			dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR, false, false, evt.text));
		}

		private function onInit(evt:Event) {
			dispatchEvent(new Event(Event.INIT));
		}

	}

}

Cette classe va charger le fichier .swf à l’URL spécifié et va renvoyer les évènements INIT et COMPLETE. Une fois ce fichier chargé, vous pouvez récupérer les clips qui ont été exportés.

Pour cela je crée le fichier media.fla qui contient une animation de chargement. Au moment où je crée le Movieclip de l’animation je coche la case « Export for ActionScript » et je nomme la class « Loader ». Une fois cela terminé je compile media.swf.

Je crée les fichiers main.fla et Main.as qui contiennent le code.

package  {
	import flash.display.MovieClip;
	import multifreid.utils.SharedLibrary;
	import flash.events.Event;

	public class Main extends MovieClip{
		var library:SharedLibrary;
		var clipLoader:MovieClip;

		public function Main() {
			// constructor code
			library = new SharedLibrary("media.swf");
			library.addEventListener(Event.COMPLETE, onComplete);
		}

		function onComplete(evt:Event)
		{
			clipLoader = library.createClip("Loader") as MovieClip;
			this.addChild(clipLoader);
			clipLoader.x = stage.stageWidth*.5;
			clipLoader.y = stage.stageHeight*.5;

		}

	}

}

Compilez main.swf et vous verrez votre animation se jouer.

Les Web Services en ActionScript 3.0

Très souvent lorsque l’on fait du jeu Facebook, on se retrouve à devoir gérer des communication client-serveur régulièrement. Au lieu d’écrire chaque envoie de requête à plusieurs reprises dans le code, j’ai créé une classe WebService qui me simplifie la tâche. Cette classe n’a pas pour but de réinventer la roue mais de faciliter l’intégration des web services.

Le but est de créer une requête HTML avec une syntaxe la plus simple possible :

webService.createRequest("url_relative_de_la_requete", {var0:val0 ,var1:val1, ...});

Une pile de requêtes?

Une autre problématique, que ma classe gère, est la gestion d’une pile de requêtes. Il m’est déjà arrivé d’avoir affaire à un serveur relativement lent et qui crash car il doit gérer trop de requête simultanément. Pour cette raison, j’ai fait en sorte que ma classe WebService gère une pile de requêtes. De plus, j’ai indiqué l’URL du serveur en variable de classe, car je considère qu’il n’y a qu’un seul serveur par projet (ce qui est vrai dans quasiment tous les cas). Mais dans le cas où ce n’est pas le cas, il suffit d’initialiser urlBase à une chaîne de caractère vide et de passer une url absolue en paramètre à createRequest.

package multiFreid.net
{
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.net.URLVariables;
	import flash.net.navigateToURL;

	/**
	 * @author MultiFreid
	 **/
	public class WebService
	{

		public static var urlBase:String;
		protected var pile:Array;
		protected var current:URLRequest;

		/**
		 * Constructor
		 * @param	plateformURL	the plateform url : "http://my.domain.net/"
		 */
		public function WebService(plateformURL:String=null)
		{
			if(plateformURL)
				urlBase = plateformURL;
			pile = new Array();
		}

Pour cela je crée une classe RequestStruct qui me permettra de gérer ma pile. Dans un autre langage de programmation j’aurai géré cela avec une structure de donnée (struct) mais l’AS3 ne gère pas cela. Cette classe n’est pas public et donc est rajouté à la fin de la classe WebService.

//...
}
import flash.net.URLLoader;
import flash.net.URLRequest;
class RequestStruct{

	private var _loader:URLLoader;
	private var _params:Object;
	private var _request:URLRequest;

	public function RequestStruct(urlLoader:URLLoader, r:URLRequest, params:Object=null) {
		_loader = urlLoader;
		_params = params?params:{};
		_request = r;
	}
	public function get request():URLRequest { return _request; }
	public function get loader():URLLoader { return _loader; }
	public function get callbackComplete():Function { return _params.onComplete as Function;}
	public function get callbackError():Function { return _params.onError as Function;
	//d'autres getters pour personnaliser vos paramètres
}

une fois que vous avez cette « structure » on va pouvoir gerer notre pile de requete.

Dans la classe WebService, on crée les getters stackSize et currentRequest qui renvoie respectivement la taille de la pile (int) et l’URLRequest en cours de traitement.

Puis on crée la fonction createRequest qui est la fonction qui va gérer la création des requête.

public function createRequest(relativeURL:String, param:Object=null){
	var loader:URLLoader = new URLLoader();
	var request:URLRequest;
	var url:String;
	url = urlBase?urlBase + relativeURL:relativeURL;
	request = new URLRequest(url);
	//...

Il va falloir maintenant ajouter les URLVariables à l’URLRequest pour cela on doit déterminer si les propriétés passées dans params sont à envoyer avec la requête ou bien s’il s’agit d’un mot clé ou non. Pour cela on crée la fonction isKeyWord (voir fonctions annexes) qui prend en paramètre le nom de la propriété (string) et renvoie un booléen.

	//...
	if (param){
		var variables:URLVariables = new URLVariables();
		for (var prop:String in param) {
			if (!isKeyWord(prop)){
				variables[prop] = param[prop];
			}
		}
		if (param.method && (param.method == URLRequestMethod.GET)){
			request.method = URLRequestMethod.GET;
		}
		else{
			request.method = URLRequestMethod.POST;
		}
		request.data = variables;
	}
	//...

Enfin on ajoute la requête à la pile et on crée les EventListeners (voir fonctions annexes).

	//...
	loader.addEventListener(Event.COMPLETE, onComplete);
	loader.addEventListener(IOErrorEvent.IO_ERROR, onFail);
	if(pile.push(new RequestStruct(loader, request, param))== 1){
		loader.load(request);
		current = request;
	}
}//end of createRequest

Utilisation de WebService

Un petit test avec notre cher ami google :

import multiFreid.net.WebService;
import flash.events.Event;
import flash.net.URLVariables;
import flash.net.URLRequestMethod;
import flash.net.navigateToURL;

WebService.urlBase="http://www.google.fr/";

var ws:WebService = new WebService();
ws.createRequest("search", {q:"test", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test1", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test2", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test3", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test4", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test5", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test6", onComplete:onComplete, method:URLRequestMethod.GET});

function onComplete(evt:Event):void
{
	trace("completed request :: ", ws.currentRequest.url, (ws.currentRequest.data as URLVariables).toString());
	navigateToURL(ws.currentRequest, "_blank")
}

vous pouvez admirer les fenêtres qui s’ouvre les unes à la suite !

N.B. : La fonction onComplete sera toujours exécutée avant le changement de requête.

Les fonctions annexes

/**
 * Test whether the string is a key word or not
 * @param	st	string to test
 * @return	true if 'st' is a keyword
 */
protected function isKeyWord(st:String):Boolean
{
	return st == "onComplete" || st == "onError" || st == "method"; //To complete when keyword are added
}
/**
 * the callback function executed when the current RequestStruct dispatch Event.COMPLETE
 * @param	evt		the Event.
 */
protected function onComplete(evt:Event) {
	var lo:RequestStruct = pile.shift() as RequestStruct;
	trace("Network ::", "REQUETES RESTANTES :", pile.length);
	if (lo.callbackComplete != null) {
		lo.callbackComplete(evt);
	}
	try
	{
		var next:RequestStruct = pile[0] as RequestStruct;
		current = next.request;
		next.loader.load(next.request);
	}
	catch (err:Error)
	{
		current=null;
	}
}
/**
 * the callback function executed when the current RequestStruct dispatch IOErrorEvent.IO_ERROR
 * @param	evt		the Event to dispatch
 */
protected function onFail(evt:IOErrorEvent) {
	var lo:RequestStruct = pile.shift() as RequestStruct;
	if (lo.callbackError != null) {
		lo.callbackError(evt);
	}
	try
	{
		var next:RequestStruct = pile[0] as RequestStruct;
		current = next.request;
		next.loader.load(next.request);
	}
	catch (err:Error)
	{
		current=null;
	}
}
/**
* Get the URL of the current RequestStruct
**/
public function get currentRequest():URLRequest{return current;}
/**
 * Get the size of the stack of request
**/
public function get stackSize():uint{return pile.length;}

Vous pouvez trouver le fichier .as complet et fonctionnel ici