Tutorial pour utiliser simplement les Pixel Shaders avec Silverlight 3
Silverlight 3 nous propose une nouveauté particulièrement intéressante pour effectuer des traitements graphiques sympathiques : les pixel shaders. En gros, cela consiste à appliquer une équation mathématique sur chacun des pixels constituant l’élément à modifier. Le langage retenu par Silverlight 3 pour ces shaders est le HLSL dont vous trouverez de la littérature en Anglais sur Wikipedia ou sur MSDN. Il ressemble à une espèce de C.
Alors tout cela est bien joli, mais faut-il pour autant être une Medal Fields de mathématique pour pouvoir jouir de cette nouvelle puissance graphique à la portée de nos petits doigts de développeurs ?
Heureusement que non ! Sinon, j’ai bien peur que je n’aurais pas pu m’en servir (je suis devenu nul en math après le BAC semblerait-il :)).
A travers ce tutorial, nous allons mettre au point la petite application Silverlight 3 ci-dessous (cliquez sur les pinguins!)
Par défaut, 2 effets sont facilement disponibles sur n’importe lequel de vos éléments. Ils se trouvent sous la propriété Effect et se nomment BlurEffect et DropShadowEffect.
Note 1:
vous trouverez à la fin de ce billet une vidéo de démonstration suivant les étapes décrites dans ce tutoriel. Note 2: contrairement à WPF 3.5 SP1, les effets de type Pixel Shaders sont rendus de manière « logicielle » (c'est-à-dire par notre CPU préféré) et non pas par le GPU de la carte graphique. Dès que vous appliquerez un effet de shader, vous perdrez de facto l’accélération matérielle sur l’élément cible. Plus d’infos sur la gestion du GPU par SL3 ici.
Allez, je vous propose de suivre quelques étapes pour découvrir tout cela sans mal de tête.
1– Créez un projet Silverlight 3 de type « Silverlight Application » que vous hébergerez dans une simple page HTML.
2 – Créez un répertoire « images » à la racine de votre projet et insérez-y 2 images de votre choix. Dans mon cas, j’ai pris les images « Penguins.jpg » et « Tulips.jpg » présentes dans les samples de Windows 7.
3 – Au sein de la grille, insérez un contrôle Image et pointez vers l’une de vos 2 images.
4 – Observez la propriété Effect du contrôle Image. Nous allons d’abord utiliser un effet d’ombre portée à l’aide de ce bout de XAML :
<Image Source="images/Penguins.jpg" Margin="20">
<Image.Effect>
<DropShadowEffect ShadowDepth="10" Direction="45" />
</Image.Effect>
</Image>
Les propriétés ShadowDepth et Direction indiquent respectivement le niveau de profondeur de l’ombre portée et la direction de celle-ci. Voici le résultat en image :
5 – Utilisons maintenant à la place un effet de flou (Blur). Pour cela, utilisez le morceau de XAML suivant :
<Image.Effect>
<BlurEffect Radius="5" />
</Image.Effect>
Voici le résultat avec cet effet :
Ne réglez pas votre téléviseur, nous contrôlons les horizontales et les verticales avec l’effet myope de Silverlight 3.
Si l’on souhaite mettre en place davantage d’effets, il faut alors créer ce que l’on appelle un « custom effect » à l’aide du langage HLSL dont je vous parlais plus haut.
Il faut écrire un .FX et cela ressemble alors à cela :
float4 main(float2 uv : TEXCOORD) : COLOR
{
float2 dir = uv - center;
float2 toPixel = uv - center; // vector from center to pixel
float distance = length(toPixel);
float2 direction = toPixel/distance;
float angle = atan2(direction.y, direction.x);
float2 wave;
sincos(frequency * distance + phase, wave.x, wave.y);
float falloff = saturate(1-distance);
falloff *= falloff;
distance += amplitude * wave.x * falloff;
sincos(angle, direction.y, direction.x);
float2 uv2 = center + distance * direction;
float lighting = saturate(wave.y * falloff) * 0.2 + 0.8;
float4 color = tex2D( implicitInputSampler, uv2 );
color.rgb *= lighting;
return color;
}
Une fois ce shader écrit, il faut le compiler en .PS à l’aide du SDK de DirectX. C’est ce fameux .PS (le shader compilé) que l’on pourra ensuite appliquer au sein d’une application Silverlight 3 comme effet personnalisé. Et c’est là que vous vous dites « mince, j’aurais du définitivement mieux suivre mes cours de Math au lieu d’aller jouer au baby foot moi ! ». En tout cas, c’est ce que je me dis de mon coté.
Heureusement, rien n’est perdu ! Pendant que nous allions jouer au baby foot, d’autres suivaient ardemment les cours de traitement du signal et autres joyeusetés. Ils ont ensuite eu la bonne idée de créer une librairie d’effets shaders compatibles avec WPF et Silverlight 3. Ils ont déposés le fruit de leur labeur sur codeplex : https://wpffx.codeplex.com/
Reprenons alors la suite de notre tutorial.
6 – Récupérez le code des librairies présentes sur CodePlex
7 – Ouvrez le projet contenu dans le répertoire « WPFSLFx\WPFSLFx\SL\SLShaderEffectLibrary » et compilez le.
8 – Ajoutez une référence à la DLL « SLShaderEffectLibrary.dll » ainsi générée dans le répertoire « Bin ».
9 – Déclarez ce nouveau namespace dans votre XAML :
xmlns:ShaderEffectLibrary="clr-namespace:ShaderEffectLibrary;assembly=SLShaderEffectLibrary"
10 – Vous avez désormais une nouvelle panoplie d’effets à votre disposition !
11 – Utilisons maintenant celui faisant des vagues :
<Image.Effect>
<ShaderEffectLibrary:RippleEffect />
</Image.Effect>
Et voici le résultat :
C’est quand même pas mal non ?
Bon maintenant, j’aimerais aller un peu plus loin en mettant en place des animations utilisant ces effets pour effectuer une jolie transition entre 2 images.
Comme nous utilisons un contrôle container de type « Grid », les éléments que l’on met dedans sont automatiquement superposés les uns sur les autres. Cela nous arrange donc pour mettre au point une animation de transition. On va en effet jouer sur la propriété d’opacité pour passer d’une image à l’autre.
12 – Revenons donc à ce XAML simplifié :
<Image x:Name="image2" Source="images/Tulips.jpg" Margin="20" />
<Image x:Name="image1" Source="images/Penguins.jpg" Margin="20" />
13 – Nous allons d’abord jouer sur les valeurs d’opacité pour mettre en place l’animation la plus simple qui soit. Pour cela, déclarez ce storyboard comme ressource au contrôle :
<UserControl.Resources>
<Storyboard x:Name="maJolieAnimation">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="image1" Storyboard.TargetProperty="(UIElement.Opacity)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
<EasingDoubleKeyFrame KeyTime="00:00:05" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
On indique ici de faire passer la propriété opacité de notre image 1 (le pingouin) de 1 (visible) à 0 (invisible) à travers une animation durant 5 secondes. Lorsque l’image 1 ne sera plus visible, nous verrons donc l’image 2 qui se trouve en dessous. Pour lancer l’animation, abonnez-vous à l’évènement MouseLeftButtonDown de l’image 1 et lancez le code suivant :
private void image1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
maJolieAnimation.Begin();
maJolieAnimation.AutoReverse = true;
}
Testez l’ensemble. En cliquant sur l’image 1, vous enclenchez l’animation de fondu. Grâce à l’AutoReverse, vous reviendrez alors automatiquement au statut de départ.
14 – Couplons maintenant à cet effet de fondu, un effet de shader animé basé sur le RippleEffect. Commencez par utiliser ce XAML pour l’image 1 :
<Image x:Name="image1" Source="images/Penguins.jpg" Margin="20" MouseLeftButtonDown="image1_MouseLeftButtonDown">
<Image.Effect>
<ShaderEffectLibrary:RippleEffect Amplitude="0" Frequency="0" />
</Image.Effect>
</Image>
On déclare ici que l’on souhaite appliquer l’effet RippleEffect qui n’a pour l’instant aucun résultat visible puisque nous avons positionné l’amplitude et la fréquence de la vague à 0.
15 – Modifions notre storyboard pour travailler sur cet effet en ajoutant ce XAML en dessous du travail effectué sur l’opacité :
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="image1" Storyboard.TargetProperty="(UIElement.Effect).(RippleEffect.Frequency)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<EasingDoubleKeyFrame KeyTime="00:00:05" Value="75"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="image1" Storyboard.TargetProperty="(UIElement.Effect).(RippleEffect.Amplitude)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<EasingDoubleKeyFrame KeyTime="00:00:05" Value="1.1"/>
</DoubleAnimationUsingKeyFrames>
Nous travaillons ici sur 2 propriétés supplémentaires : la valeur de fréquence et d’amplitude allant respectivement de 0 à 75 et de 0 à 1.1 pendant 5 secondes. Pour rappel, nous faisons également varier l’opacité dans la même tranche de temps.
Compilez et testez l’application pour vérifier que la jolie animation fonctionne bien.
Alors logiquement, tel que je vous connais, vous devriez me dire 2 choses :
1 – Comment apprendre à connaître le fonctionnement de ces nouveaux effets (les propriétés à manipuler, le résultat à l’écran, etc.) ?
2 – Suis-je obligé d’écrire le XAML des animations à la main ?
Pour la 1ère question, la réponse peut passer par l’utilisation d’un outil appelé Shazzam se trouvant ici : https://shazzam-tool.com/ . Ce dernier permet de tester différents pixels shaders avec un éditeur de .FX intégré. Vous trouverez des informations écrites par l’auteur de l’outil ici.
Pour la 2ème question, je vous propose d’utiliser un outil plutôt doué pour la génération de XAML… j’ai nommé Express Blend 3 !
16 – Ouvrez la page MainPage.xaml avec Blend 3
On voit alors plusieurs choses. Tout d’abord, on retrouve bien l’effet actuellement appliqué à notre première image :
En cliquant sur « New », vous pouvez également retrouver l’ensemble des effets de notre librairie :
Cela indique que nous allons pouvoir plutôt utiliser Blend 3 pour appliquer des effets à nos éléments plutôt que de taper le XAML à la main dans Visual Studio. Par ailleurs, on peut également voir et éditer l’animation que nous avons mise en place :
17 – Nous allons alors en profiter pour légèrement améliorer l’animation actuellement en place sur la variation de l’opacité. Après avoir sélectionné « maJolieAnimation », cliquez sur l’élément « Opacity » de l’objet « image1 » :
Au lieu d’avoir une animation linéaire, j’aimerais plutôt avoir une animation relativement lente au début et s’accélérant vite à la fin pour que nous ayons plus le temps de voir l’effet shader animé sur l’image 1. Pour cela, cliquez sur la combo « EasyFunction » et choisissez la 1ère de type « Quintic In » :
Cela devrait alors vous produire le XAML suivant :
<UserControl x:Class="TestPSLibrary.MainPage"
xmlns=https://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=https://schemas.microsoft.com/winfx/2006/xaml
xmlns:d=https://schemas.microsoft.com/expression/blend/2008
mlns:mc=https://schemas.openxmlformats.org/markup-compatibility/2006
xmlns:ShaderEffectLibrary="clr-namespace:ShaderEffectLibrary;assembly=SLShaderEffectLibrary"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<UserControl.Resources>
<Storyboard x:Name="maJolieAnimation">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="image1" Storyboard.TargetProperty="(UIElement.Opacity)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="1">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:05" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="image1" Storyboard.TargetProperty="(UIElement.Effect).(RippleEffect.Frequency)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<EasingDoubleKeyFrame KeyTime="00:00:05" Value="75"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="image1" Storyboard.TargetProperty="(UIElement.Effect).(RippleEffect.Amplitude)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<EasingDoubleKeyFrame KeyTime="00:00:05" Value="1.1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<Image x:Name="image2" Source="images/Tulips.jpg" Margin="20" />
<Image x:Name="image1" Source="images/Penguins.jpg" Margin="20" MouseLeftButtonDown="image1_MouseLeftButtonDown">
<Image.Effect>
<ShaderEffectLibrary:RippleEffect Amplitude="0" Frequency="0" />
</Image.Effect>
</Image>
</Grid>
</UserControl>
Sauvegardez et recompilez. Si tout s’est bien passé, vous devriez alors obtenir l’application Silverlight 3 présentée au début de ce billet.
Voici le projet Visual Studio 2008 correspondant à cet article :
Pour terminer, voici une vidéo de 15 min où je me propose de suivre ces mêmes étapes pour vous (double-cliquez dessus pour le plein écran) :
Pour discuter davantage sur ce sujet, n'hésitez pas à nous rejoindre sur ce fil de discussion.
Bon Pixel Shaders et à bientôt !
David
Comments
Anonymous
July 28, 2009
Bonjour David, dis comment t'as fait pour avoir le ripple qui marche ? quand je compile la lib des wpffx avec le SDK DirectX cet effet ne passe pas car il utilise trop de slots. Je suis obligé de supprimer cet effet pour faire passer le reste. Sinon à titre indicatif j'ai publié presque en même temps un billet sur le même sujet, avec exemple live aussi, si ça intéresse quelques lecteurs : http://www.e-naxos.com/Blog/post.aspx?id=4f654a40-78b6-4811-90ff-a203f42c91b8Anonymous
July 28, 2009
Salut Olivier, J'ai eu le même soucis avec la compilation du Ripple. Je ne me suis pas cassé la tête dans cet exemple, j'ai récupéré le shader déjà compilé depuis le projet codeplex. Merci pour ton article. Je m'en vais d'ailleurs y faire un petit commentaire sur la gestion du GPU car il y a quelques imprécisions. Bye, DavidAnonymous
July 29, 2009
Merci de ta réponse. ça reste quand même mystérieux cet effet qu'on ne pas compiler mais qui existe sous forme compilée... comment l'ont-ils compilé ? Heureusement tout n'est pas aussi mystérieux et tes éclaircissements sur l'accélération GPU permettent d'y voir plus clair sur le sujet !