Frédéric Mélantois
WPF : Effets et Pixel Shaders
Le SP1 du Framework .NET 3.5 apporte à WPF un certain nombre d’améliorations dont les effets sur les images. Une « fenêtre » pour l’utilisation des pixel shaders de DirectX a été ouverte.
Par Frédéric Mélantois publié le 12/01/2009 à 09:06, lu 4024 fois, 6 pages
 4 | Quelques exemples simples de pixel shader
Nous avons pu nous apercevoir que la technique de mise en œuvre d’un effet s’appuyant sur un pixel shader est relativement simple. Etudions d’autres exemples afin de maîtriser un peu plus le langage HLSL. Nous en profiterons pour découvrir d’autres effets.
Un effet classique est de mettre un élément en couleur en niveau de gris. Pour cela, on utilise l’approximation suivante : on multiplie la valeur de la composante Rouge par 0.3, verte par 0.59, bleu e par 0.11. Le code HLSL peut s’écrire de cette manière:

sampler2D ImplicitInput : register(s0);

 

float4 main (float2 uv : TEXCOORD) : COLOR

{

    float4 color;

 

    color = tex2D( ImplicitInput, uv);

    color.rgb = color.r*0.3 + color.g*0.59 + color.b*0.11;

 

    return color;

}

Par l’instruction « text2D », nous obtenons la valeur de la couleur du pixel aux coordonnées « uv » de la texture « ImplicitInput ». Nous pouvons affecter une nouvelle valeur aux composantes r, g, b c'est-à-dire rouge, verte, bleue. Cette nouvelle valeur est calculée selon la formule :

Rouge * 0.3 + Vert * 0.59 + Bleu * 0.11

Avec le langage HLSL, il y a moyen d’optimiser ce calcul en utilisant les fonctions spécialisées de HLSL comme celles-ci :

sampler2D ImplicitInput : register(s0);

 

float4 main (float2 uv : TEXCOORD) : COLOR

{

    float4 color;

 

    float3 nivGris = float3(0.30, 0.59, 0.11);

    color.rgb = dot(tex2D( ImplicitInput, uv).rgb,nivGris);

    color.a = tex2D( ImplicitInput, uv) ;

 

    return color;

}

La fonction float « n »(…) permet de condenser différentes valeurs. Par exemple, « float2 = float2(x,y) ; » permet de créer un vecteur avec une abscisse et une ordonnée. Avec la fonction float3(r,g,b), nous créons un vecteur avec les composantes de couleurs. La fonction dot(v1,v2) du langage HLSL permet entre autres de multiplier les composantes de deux vecteurs de même dimension. C’est cette fonction que nous appliquons dans l’exemple ci-dessus.
Il serait intéressant de comparer les instructions assembleur dans les deux cas de figures. Dans notre première façon d’opérer, nous obtenons, grâce à l’utilitaire « fxc.exe » et sa directive de compilation « /Fx » ou « /Fc », le code assembleur suivant :

//

// Generated by Microsoft (R) HLSL Shader Compiler 9.24.949.2307

//

//   fxc /T ps_2_0 /E main /Fx MyEffect.txt MyEffect.fx

//

//

// Parameters:

//

//   sampler2D ImplicitInput;

//

//

// Registers:

//

//   Name          Reg   Size

//   ------------- ----- ----

//   ImplicitInput s0       1

//

 

    ps_2_0

    def c0, 0.589999974, 0.300000012, 0.109999999, 0

    dcl t0.xy

    dcl_2d s0

    texld r0, t0, s0

    mul r1.w, r0.y, c0.x

    mad r1.x, r0.x, c0.y, r1.w

    mad r0.xyz, r0.z, c0.z, r1.x

    mov oC0, r0

 

// approximately 5 instruction slots used (1 texture, 4 arithmetic)

Dans le second cas, avec les fonctions spécifiques de HLSL, nous obtenons cette série d’instructions assembleur :

//

// Generated by Microsoft (R) HLSL Shader Compiler 9.24.949.2307

//

//   fxc /T ps_2_0 /E main /Fx MyEffect.txt MyEffect.fx

//

//

// Parameters:

//

//   sampler2D ImplicitInput;

//

//

// Registers:

//

//   Name          Reg   Size

//   ------------- ----- ----

//   ImplicitInput s0       1

//

 

    ps_2_0

    def c0, 0.300000012, 0.589999974, 0.109999999, 0

    dcl t0.xy

    dcl_2d s0

    texld r0, t0, s0

    dp3 r1.z, r0, c0

    mov r1.w, r0.x

    mov r1.xyz, r1.z

    mov oC0, r1

 

// approximately 5 instruction slots used (1 texture, 4 arithmetic)

Les instructions « mul » et « mad » sont « remplacés par l’instruction « dp3 ». Notez bien que si nous ne souhaitions pas conserver la valeur de la couche alpha soit :

color.a = tex2D( ImplicitInput, uv) ;

Nous réduirions de deux instructions assembleur « mov » le code ci-dessus. Vérifions ceci :

sampler2D ImplicitInput : register(s0);

 

float4 main (float2 uv : TEXCOORD) : COLOR

{

    float4 color;

    float3 nivGris = float3(0.30, 0.59, 0.11);

    color.rgb = dot(tex2D( ImplicitInput, uv).rgb,nivGris);

 

 

    return color;

}

 

 

ps_2_0

    def c0, 0.300000012, 0.589999974, 0.109999999, 0

    dcl t0.xy

    dcl_2d s0

    texld r0, t0, s0

    dp3 r0.xyz, r0, c0

    mov oC0, r0

 

// approximately 3 instruction slots used (1 texture, 2 arithmetic)

Nous avons effectivement bien une réduction de 2 instructions ! Une question que l’on peut se poser : que se passe t-il lorsque nous utilisons la fonction « float4(r,g,b,a) » ?

sampler2D ImplicitInput : register(s0);

 

float4 main (float2 uv : TEXCOORD) : COLOR

{

    float4 color;

    float4 nivGris = float4(0.30, 0.59, 0.11, 0);

    color = dot(tex2D( ImplicitInput, uv),nivGris);

 

    return color;

}

 

ps_2_0

    def c0, 0.300000012, 0.589999974, 0.109999999, 0

    dcl t0.xy

    dcl_2d s0

    texld r0, t0, s0

    dp3 r0, r0, c0

    mov oC0, r0

 

// approximately 3 instruction slots used (1 texture, 2 arithmetic)

Nous constatons que le compilateur utilise toujours l’instruction assembleur « dp3 » alors que l’on s’attendait à avoir l’instruction « dp4 ». Nous sommes donc en présence d’une optimisation effectuée par le compilateur. Nous pouvons le vérifier en effet, en changeant la valeur 0 du vecteur « nivGris » par 0.001 :

sampler2D ImplicitInput : register(s0);

 

float4 main (float2 uv : TEXCOORD) : COLOR

{

    float4 color;

    float4 nivGris = float4(0.30, 0.59, 0.11, 0.001);

    color = dot(tex2D( ImplicitInput, uv),nivGris);

 

    return color;

}

 

ps_2_0

    def c0, 0.300000012, 0.589999974, 0.109999999, 0.00100000005

    dcl t0.xy

    dcl_2d s0

    texld r0, t0, s0

    dp4 r0, r0, c0

    mov oC0, r0

 

// approximately 3 instruction slots used (1 texture, 2 arithmetic)

Nous constatons bien que quand le quatrième composant du vecteur «nivGris » n’a pas une valeur égale à 0, l’instruction assembleur « dp4 » est utilisée. Nous venons donc de montrer encore une fois que le compilateur avait une forte capacité d’optimisation.
Essayons de construire des effets un peu plus élaborés. Jusqu’à présent, nous ne transmettions pas de paramètres outre la texture au pixel shader. Nous allons maintenant étudier comment transmettre une constante à celui-ci. Pour cela, nous pourrions construire un effet qui permet de modifier l’intensité lumineuse d’une image. Généralement, il est usage de réaliser l’opération suivante :

couleur corrigée = couleur + intensité

Mais cette technique présente le désavantage d’affecter tous les pixels de la même façon. Nous allons donc plutôt prendre comme exemple la technique de correction d’exposition. Quand nous prenons une photo, celle-ci n’est pas toujours prise dans des conditions idéales de lumière. On peut corriger l’exposition de la photo non pas en ajoutant ou soustrayant par une valeur, comme nous venons de le voir précédemment, mais en appliquant une fonction non linéaire comme la fonction exponentielle.
La problématique est qu’il ne faut pas dépasser la valeur de l’intensité maximum. Il nous faut donc une fonction qui permette de se rapprocher de zéro et de tendre vers 1 suivant deux bornes définies. On constate que la fonction exp(-x) tend vers 0 quand x se rapproche de l’infini (c’est à dire de la valeur maximum) et est égal à 1 quand x est égal à 0. La formule suivante permet d’établir ce que nous voulons (c’est à dire mettre en valeur les pixels de fortes intensités) :

1 - exp(-x)

avec x = couleur * facteur

avec facteur >= 0

Pour mettre en œuvre cette formule, nous avons besoin de transmettre une constante au pixel shader. Nous allons donc modifier la classe héritée de ShaderEffect afin qu’elle puisse prendre en charge un nouveau paramètre compris entre 0 et 10 par exemple. Reprenons le code tel que nous l’avions laissé au deuxième chapitre de cet article :

public class MyEffect : ShaderEffect

{

    public MyEffect()

    {

        this.PixelShader = _pixelShader;

        UpdateShaderValue(ImplicitInputProperty);

    }

 

    private static PixelShader _pixelShader = new PixelShader()

                { UriSource = new Uri(@"pack://application:,,,/MyEffect;component/MyEffect.ps") };

 

    public static readonly DependencyProperty ImplicitInputProperty =

                    ShaderEffect.RegisterPixelShaderSamplerProperty("ImplicitInput", typeof(MyEffect), 0);

    public Brush ImplicitInput

    {

        get { return (Brush)GetValue(ImplicitInputProperty); }

        set { SetValue(ImplicitInputProperty, value); }

    }

}

Il faut ajouter une nouvelle propriété « facteur » :

public static readonly DependencyProperty FacteurProperty = DependencyProperty.Register(

        "Facteur", typeof(double), typeof(MyEffect),

            new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0))

            );

    public double Facteur

    {

        get { return (double)GetValue(FacteurProperty); }

        set { SetValue(FacteurProperty, value); }

    }

Nous utilisons à nouveau les propriétés de dépendances. Le facteur sera de type « double ». Nous faisons usage de la méthode statique de la classe « ShaderEffect » : « PixelShaderConstantCallback » afin de faire la relation entre la propriété de dépendance « FacteurProperty » et la constante de registre que nous définirons un peu plus loin lorsque nous écrirons le code HLSL du pixel shader. Notez bien que la méthode prend en paramètre l’index du registre de la constante, ici zéro.
N’hésitons pas à étudier les différents constructeurs de la classe « UIPropertyMetaData ». En effet, si nous ajoutons un paramètre supplémentaire, nous pourrons manipuler la valeur de « facteur » avant qu’elle ne soit transmise au pixel shader. Cette possibilité est essentielle ! Rappelez-vous bien que l’accès au mini-programme pixel shader se produit pixel par pixel. On comprend alors que si nous devons contrôler la valeur de la constante « facteur », il est mieux de le faire en dehors du mini-programme afin que ce contrôle ne soit pas effectué inutilement à chaque pixel. Regardons la documentation concernant un des constructeurs de « UIPropertyMetaData » :
 
/content/13791ad3-9dcd-44e1-8694-84f713c4a868/image9.jpeg
 
Nous pouvons donc mettre en œuvre facilement notre contrôle des valeurs pouvant être prises par « Facteur » :

public static readonly DependencyProperty FacteurProperty = DependencyProperty.Register(

        "Facteur", typeof(double), typeof(MyEffect),

            new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0), CoerceFacteur)

            );

 

    private static object CoerceFacteur(DependencyObject dpdObj, object valeur)

    {

        MyEffect effect = dpdObj as MyEffect;

        double facteur = 0;

        if (effect != null && double.TryParse(valeur.ToString(), out facteur))

        {

            if (facteur < 0.0 || facteur > 10.0)

            {

                return effect.Facteur;

            }

        }

        return facteur;

    }

Il ne nous reste plus qu’à mettre à jour le constructeur de « MyEffect » afin qu’il prenne en compte la nouvelle propriété « Facteur ».

public MyEffect()

{

    this.PixelShader = _pixelShader;

    UpdateShaderValue(ImplicitInputProperty);

    UpdateShaderValue(FacteurProperty);   

}

Nous venons de terminer la mise au point de la partie WPF. Consacrons-nous maintenant à écrire le code HLSL permettant de produire un effet de correction d’exposition :

sampler2D ImplicitInput : register(s0);

float facteur : register(c0)

 

float4 main (float2 uv : TEXCOORD) : COLOR

{

    float4 coul;

    coul = tex2D(implicitInput,uv);

 

    coul.rgb = 1.0 - exp(-(coul.rgb * facteur));

    return coul;

}

La constante « facteur » est notifiée à l’index 0 du registre. Le langage HLSL offre un grand nombre de fonctions mathématiques permettant d’effectuer toutes sortes de calcul. Aussi, implémenter un mini-programme de correction d’exposition devient un vrai jeu d’enfant. Il nous a fallu que très peu de lignes de code pour mettre en place un effet de correction d’exposition.
Voici un exemple d’application de correction d’exposition sur une photo prise un matin d’hiver sur le bord de Loire. La luminosité ambiante est faible. Grâce à l’effet que nous venons de construire, nous pouvons corriger l’exposition. Les pixels à plus fortes valeurs seront plus fortement affectés sans que les zones sombres ne blanchissent trop.
 
/content/13791ad3-9dcd-44e1-8694-84f713c4a868/image10.jpeg
 
J’attire votre attention sur le fait que vous pouvez étendre les possibilités de vos effets avec les animations de Windows Presentation Foundation. Vous pouvez coupler la constante « facteur » avec un « DoubleAnimation » sous WPF suivant l’interaction produite par l’utilisateur. Cela vous permettra de réaliser des effets très riches sur vos visuels hérités de « UIElement ».
Enfin, je n’ai cessé de vous parler de constante à propos de la propriété de dépendance « FacteurProperty ». C’est que du côté du pixel shader, il s’agit bien d’une constante. En effet, le code HLSL suivant provoque une erreur :

factor -= 1;

 
» Démarrer une discussion
 
Discussion démarée par tanuki le 11/05/2009 à 18:20, 1 commentaire(s).