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 :

SL3PS001 par vous

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 :

SL3PS002 par vous
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 !

SL3PS003 par vous

11 – Utilisons maintenant celui faisant des vagues :

<Image.Effect>
    <ShaderEffectLibrary:RippleEffect />
</Image.Effect>

Et voici le résultat :

SL3PS004 par vous  

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.  

SL3PS004b par vous  

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

SL3PS005 par vous

On voit alors plusieurs choses. Tout d’abord, on retrouve bien l’effet actuellement appliqué à notre première image :  

SL3PS006 par vous

En cliquant sur « New », vous pouvez également retrouver l’ensemble des effets de notre librairie :  

SL3PS007 par vous  

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 : 

 SL3PS008 par vous

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 » :

 

SL3PS009 par vous

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 » :

SL3PS010 par vous  

 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) :

Get Microsoft Silverlight

Pour discuter davantage sur ce sujet, n'hésitez pas à nous rejoindre sur ce fil de discussion.

Bon Pixel Shaders et à bientôt !

David