L’une des nouveautés apportées par le flash player 10 est la possibilité de créer nos propres filtres via les shaders. La création de ces shaders se fait via Adobe Pixel Bender, que vous pouvez télécharger ici (actuellement, la preview release 4 est la plus récente). Si vous voulez compiler les sources AS qui suivront, suivez ce lien afin de cibler le player 10 pendant une compilation.
Pixel Bender
Adobe Pixel Bender permet d’élaborer des algorithmes de manipulation d’images dans un langage dont la syntaxe se rapproche fortement du C/C++. En effet, Pixel Bender est basé sur
GLSL, un langage de shaders qu’on couple avec OpenGL. L’avantage est l’exportation de ces algorithmes dans un bytecode indépendant du matériel (liste des cartes graphiques actuellement supportées).
Ce bytecode sera ensuite utilisé par le Flash Player. Néanmoins il faut savoir que Pixel Bender n’est pas dedié pour Flash. Ainsi, l’intégration dans Flash n’est pas parfaite à l’heure actuelle, et un certain nombre de limitations sont présentes, notamment :
- Fonctions de type Region non-supportées. Ells sont utiles voire nécessaires lorsqu’on écrit des algorithmes manipulant les pixels adjacents d’un pixel spécifique. On se cantonne donc pour l’instant à des manipulations basiques de type
Pixel In -> Pixel Outet non(Pixel In, ..., Pixel In) -> Pixel Out. - Fonction
evaluateDependents()non-supportée. Cette fonction permet de pré-calculer les valeurs qui seront constantes pour l’ensemble du processus de l’image (c’est-à-dire indépendante des valeurs de pixels de l’image) et dépendent par exemple des paramètres spécifiés. Ces valeurs ne sont alors calculées qu’une seule fois pour l’image. N’étant pas supportée pour l’instant, nous sommes obligés de faire refaire le calcul à chaque évaluation de pixel, ce qui augmente considérablement le temps de calcul global. C’est assez gênant car ces pré-calculs sont quasi-systématiques, ne serait-ce que pour remettre à l’échelle des paramètres spécifiés. Espérons que cela sera corrigé pour la release finale ;). - Utilisation de bibliothèques et fonctions custom non-supportée. Gloups… Obligé de copier-coller le code commun à chaque fois, donc réutilisabilité inexistante à l’heure actuelle.
Afin de vous assurer de la compatibilité d’un kernel pour Flash Player, faites Build -> Turn on Flash Player Warnings and Errors dans Pixel Bender.
Avant d’aborder la partie d’intégration dans Flash, voyons comment se structure un algorithme pour Pixel Bender à l’aide d’un exemple simple.
L’algorithme de cet exemple permet de modifier la luminosité et le contraste de l’image d’entrée.
Il faut tout d’abord déclarer la version du langage utilisé :
<languageVersion : 1.0;>
Puis déclarer le kernel (l’algorithme) avec éventuellement des metadata :
kernel ContrastLightnessFilter
<
namespace : "http://blog.lunar-dev.net::Filters";
vendor : "Vincent Petithory";
version : 1;
description : "A filter to adjust the contrast and lightness of an image";
>
{
// Kernel members
}
A l’instar d’une classe, nous pouvons déclarer des variables et des méthodes pour un kernel.
La compatibilité avec le Flash Player 10 nous restreint pour l’instant à l’utilisation d’une seule et une unique méthode : void evaluatePixel(). Cette méthode est appelée pour chaque pixel de l’image.
Pour le filtre d’exemple, j’ai besoin de déclarer les variables représentant l’image d’entrée ainsi que le pixel générique de sortie dont on fixe la valeur dans la méthode evaluatePixel().
input image4 src; output pixel4 result;
L’image ARGB (image4) est une entrée du kernel, je précède donc sa déclaration par le mot-clé input. Même chose avec le pixel ARGB (pixel4) de sortie qui sera précédé du mot-clé output.
Un kernel doit toujours posséder une sortie, en revanche les entrées sont optionelles.
Il faut maintenant ajouter les paramètres de contraste et de luminosité (mot clé parameter).
Tout comme le kernel, on peut ajouter des metadata à ces paramètres. Leur signification est évidente. Cela permet au passage dans Pixel Bender de générer automatiquement un UI pour chaque paramètre tenant compte des bornes données. Pratique pour faire les tests :).
Attention cependant car ces bornes et valeurs par défaut ne seront pas utilisées dans Flash. C’est au développeur de se charger de maintenir les valeurs dans leur intervalle s’il le souhaite. Cela peut être fait ou dans le kernel, ou dans Actionscript. Je conseille plutôt de faire ceci dans la partie Actionscript pour deux raisons :
- afin d’avoir des valeurs cohérentes : si le kernel modifie lui-même l’un des paramètres et qu’on accède à sa valeur côté Actionscript, on retrouve la valeur initialement transmise, incorrecte, car différente de celle réellement utilisée dans le kernel.
- Ces metadata sont facilement accessibles via les classes ShaderData et ShaderParameter. Un exemple est donné plus loin dans les sources de la démo de l’article.
parameter float contrast < defaultValue: 0.0; minValue: -100.0; maxValue: 100.0; >; parameter float lightness < defaultValue: 0.0; minValue: -100.0; maxValue: 100.0; >;
Il reste à écrire le traitement de l’image dans la méthode evaluatePixel() :
void evaluatePixel()
{
// On récupère la valeur du pixel actuellement traité
pixel4 rgbIn = sampleNearest(src,outCoord());
// Les deux lignes suivantes auraient été parfaites dans la méthode evaluateDependents()
float c = contrast*0.01;
float l = lightness*0.01;
// calcul de coefficients
pixel3 cpx = -pixel3(0.5)*c + rgbIn.rgb*(1.0 + c);
pixel3 lpx = pixel3(0.5)*l + cpx*(1.0 + l);
// Assignation des composantes RGB
result.rgb = (cpx + lpx)*0.5;
// On conserve la couche alpha d'origine
result.a = rgbIn.a;
}
Pour tester ce kernel dans Pixel Bender, il faut passer une image (CTRL + 1) puis cliquer Run (F5).
Vous devez voir l’image non-modifiée apparaître à l’écran ainsi que deux sliders pour les paramètres lightness et contrast dans la partie droite de l’interface.
Testez le bon fonctionnement, puis faites File -> Export Kernel Filter for Flash Player (source)
Intégration dans Flash
Il y a plusieurs manières d’utiliser les shaders dans Flash, via la classe flash.display.Shader :
- Avec la classe
flash.filters.ShaderFilteret la proprietéflash.display.DisplayObject.filtersou la méthodeflash.display.BitmapData.applyFilter()comme un filtre. - Avec la proprieté
flash.display.DisplayObject.blendShadercomme mode de fusion de deux images ou plus. - Avec la méthode
flash.display.Graphics.beginShaderFill()en conjonction avec les autres methodes de dessin de la classe Graphics. - Avec la classe
flash.display.ShaderJobafin de faire des traitements conséquents sur une collection de nombre.
Pour chacune de ces utilisations, le shader écrit doit posséder des caractéristiques entrées-sorties spécifiques. Par exemple, l’utilisation en tant que filtre exige d’avoir un couple entrée-sortie image4/pixel4, ce qui est le cas de notre exemple. Je traiterai les autres cas dans des articles ultérieurs :).
Voici le code permettant d’appliquer le shader précédent :
package
{
import flash.display.Bitmap;
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.filters.ShaderFilter;
public class TestShader extends Sprite
{
[Embed(source='img.jpg')]
private static var ImgClass:Class;
[Embed(source="ContrastLightnessFilter.pbj", mimeType="application/octet-stream")]
private static var ShaderByteCode:Class;
public function TestShader()
{
super();
// pas de déformation de l’image
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
// Création d’un bitmap à partir de l’image embarquée
var image:Bitmap = new ImgClass() as Bitmap;
addChild(image);
// Création du shader à partir du bytecode embarqué
var shader:Shader = new Shader(new ShaderByteCode());
/* ou encore :
* var shader:Shader = new Shader();
* shader.byteCode = new ShaderByteCode();
*/
// On donne des valeurs aux paramètres
shader.data.lightness.value[0] = -10;
shader.data.contrast.value[0] = 20;
// On crée une instance de ShaderFilter en lui
donnant le shader comme paramètre du constructeur
var shaderFilter:ShaderFilter = new ShaderFilter(shader);
// On applique le filtre de la même manière qu’un filtre habituel
image.filters = [shaderFilter];
/*
* ou encore en l’appliquant au bitmap data :
* image.bitmapData.applyFilter(image.bitmapData, image.bitmapData.rect, new Point(), shaderFilter);
*/
}
}
}
Un point important à noter est que la classe ShaderFilter peut être étendue, contrairement à BitmapFilter qui ne l’était pas. Vous avez pu constaté qu’il est peu pratique de paramétrer le shader si l’on ne possède pas le code source.
Une bonne pratique est d’étendre la classe ShaderFilter pour chacun de nos filtres afin de rendre l’utilisation facile en exposant les paramètres du shader. Le créateur du filtre est ainsi responsable de gérer les contraintes liées à l’utilisation du shader. Lorsqu’on distribue ainsi un filtre, cela se compose d’une classe et d’un bytecode. L’implémentation de la classe est au choix. Personnellement, je choisis simplement de faire des getter/setter virtuels par paramètre pour les filtres les plus simples.
Voici une démo (sources) de ce filtre dans laquelle j’utilise une classe étendant ShaderFilter :
(Player 10 requis).
L’utilisation de cette classe est au final similaire à celle d’un filtre classique :
var filter:ContrastLightnessFilter = new ContrastLightnessFilter(-20, 10); // with a DisplayObject or a subclass myDisplayObject.filters = [filter]; // or with a BitmapData object myBitmapData.applyFilter(myBitmapData, myBitmapData.rect, new Point(), filter);
N.B. : Pour les interessés, vous pourrez voir dans les sources un exemple de l’utilisation de la nouvelle méthode flash.net.FileReference.load(), permettant de charger facilement des fichiers depuis le disque local de l’utilisateur. Ici je l’utilise en conjonction avec la méthode Loader.loadBytes() pour charger des images de l’utilisateur.
Cette nouveauté du Flash Player 10 semble offrir d’énormes possibilités, et je ne doute pas que des choses très intéressantes sortiront de ceci. Mais pour cela, il faut espérer que la compatibilité Flash Player/Pixel Bender s’améliore pour pouvoir utiliser des kernels plus complexes.
Je parlerai dans 2 autres articles de la création de modes de fusion d’images (blend modes) et de l’utilisation de ShaderJob pour des calculs lourds en parallèle.
14/08/08 à 06:08:39


