Frédéric Mélantois
WPF : Les transformations et les calculs matriciels
Le framework WPF permet d’effectuer un grand nombre de transformations sur des éléments héritant de la classe UIElement. Nous allons détailler les calculs matriciels.
Par Frédéric Mélantois publié le 16/11/2008 à 22:30, lu 2398 fois, 5 pages
 3 | Sous le capot des transformations WPF
L’équipe WPF de Microsoft a choisi de limiter les calculs afin d’optimiser les performances. C’est ainsi que seules les transformations affines sont prises en compte. La troisième colonne de la matrice disparait des calculs. Une classe Matrix dans l’espace de nom « System.Windows.Media » offre plusieurs propriétés :
M11, M12, M21, M22 correspondent aux deux premières colonnes et rangées de la matrice. La troisième colonne n’est pas représentée.
Les propriétés OffsetX, OffsetY servent par exemple aux translations. Il s’agit des deux premières colonnes de la troisième rangée de la matrice.
Nous allons regarder avec l’aide de l’utilitaire Reflector quelques méthodes pour avoir un aperçu des optimisations proposées par Microsoft. La multiplication, par définition très coûteuse en performance, a été optimisée afin de réduire le temps de calcul. Dans le Framework WPF, chaque matrice est caractérisée par un type :

[Flags]

internal enum MatrixTypes

{

    TRANSFORM_IS_IDENTITY = 0,

    TRANSFORM_IS_SCALING = 2,

    TRANSFORM_IS_TRANSLATION = 1,

    TRANSFORM_IS_UNKNOWN = 4

}

De sorte que l’on peut très largement réaliser des optimisations pour la multiplication :

internal static void MultiplyMatrix(ref Matrix matrix1, ref Matrix matrix2)

{

    MatrixTypes types = matrix1._type;

    MatrixTypes types2 = matrix2._type;

    if (types2 != MatrixTypes.TRANSFORM_IS_IDENTITY)

    {

        if (types == MatrixTypes.TRANSFORM_IS_IDENTITY)

        {

            matrix1 = matrix2;

        }

        else if (types2 == MatrixTypes.TRANSFORM_IS_TRANSLATION)

        {

            matrix1._offsetX += matrix2._offsetX;

            matrix1._offsetY += matrix2._offsetY;

            if (types != MatrixTypes.TRANSFORM_IS_UNKNOWN)

            {

                matrix1._type |= MatrixTypes.TRANSFORM_IS_TRANSLATION;

            }

        }

        else if (types == MatrixTypes.TRANSFORM_IS_TRANSLATION)

        {

            double num = matrix1._offsetX;

            double num2 = matrix1._offsetY;

            matrix1 = matrix2;

            matrix1._offsetX = ((num * matrix2._m11) + (num2 * matrix2._m21)) + matrix2._offsetX;

            matrix1._offsetY = ((num * matrix2._m12) + (num2 * matrix2._m22)) + matrix2._offsetY;

            if (types2 == MatrixTypes.TRANSFORM_IS_UNKNOWN)

            {

                matrix1._type = MatrixTypes.TRANSFORM_IS_UNKNOWN;

            }

            else

            {

                matrix1._type = MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION;

            }

        }

        else

        {

            switch (((((int)types) << 4) | types2))

            {

                case 0x22:

                    matrix1._m11 *= matrix2._m11;

                    matrix1._m22 *= matrix2._m22;

                    return;

 

                case 0x23:

                    matrix1._m11 *= matrix2._m11;

                    matrix1._m22 *= matrix2._m22;

                    matrix1._offsetX = matrix2._offsetX;

                    matrix1._offsetY = matrix2._offsetY;

                    matrix1._type = MatrixTypes.TRANSFORM_IS_SCALING

                                    | MatrixTypes.TRANSFORM_IS_TRANSLATION;

                    return;

 

                case 0x24:

                case 0x34:

                case 0x42:

                case 0x43:

                case 0x44:

                    matrix1 = new Matrix((matrix1._m11 * matrix2._m11)

                            + (matrix1._m12 * matrix2._m21),

                            (matrix1._m11 * matrix2._m12)

                            + (matrix1._m12 * matrix2._m22),

                            (matrix1._m21 * matrix2._m11)

                            + (matrix1._m22 * matrix2._m21),

                            (matrix1._m21 * matrix2._m12)

                            + (matrix1._m22 * matrix2._m22),

                            ((matrix1._offsetX * matrix2._m11)

                            + (matrix1._offsetY * matrix2._m21))

                            + matrix2._offsetX,

                            ((matrix1._offsetX * matrix2._m12)

                            + (matrix1._offsetY * matrix2._m22))

                            + matrix2._offsetY);

                    return;

 

                case 50:

                    matrix1._m11 *= matrix2._m11;

                    matrix1._m22 *= matrix2._m22;

                    matrix1._offsetX *= matrix2._m11;

                    matrix1._offsetY *= matrix2._m22;

                    return;

 

                case 0x33:

                    matrix1._m11 *= matrix2._m11;

                    matrix1._m22 *= matrix2._m22;

                    matrix1._offsetX = (matrix2._m11 * matrix1._offsetX) + matrix2._offsetX;

                    matrix1._offsetY = (matrix2._m22 * matrix1._offsetY) + matrix2._offsetY;

                    return;

            }

        }

    }

}

La rotation suit les mêmes calculs théoriques que nous avons précédemment établi :

internal static Matrix CreateRotationRadians(double angle, double centerX, double centerY)

{

    Matrix matrix = new Matrix();

    double num = Math.Sin(angle);

    double num2 = Math.Cos(angle);

    double offsetX = (centerX * (1.0 - num2)) + (centerY * num);

    double offsetY = (centerY * (1.0 - num2)) - (centerX * num);

    matrix.SetMatrix(num2, num, -num, num2, offsetX, offsetY, MatrixTypes.TRANSFORM_IS_UNKNOWN);

    return matrix;

}

Si nous effectuons une brève analyse de la façon dont les combinaisons sont regroupées dans TransformGroup de l’espace de nom « System.Windows.Media », nous pouvons constater qu’il s’agit d’une collection de transformations dont la valeur finale représente les multiplications successives des matrices correspondant à ces transformations :

public override Matrix get_Value()

{

    base.ReadPreamble();

    TransformCollection children = this.Children;

    if ((children == null) || (children.Count == 0))

    {

        return new Matrix();

    }

    Matrix matrix = children.Internal_GetItem(0).Value;

    for (int i = 1; i < children.Count; i++)

    {

        matrix *= children.Internal_GetItem(i).Value;

    }

    return matrix;

}

Grâce à Reflector, nous pouvons confirmer que des opérations de multiplications sont effectuées. De plus, chaque transformation (RotateTransform, ScaleTransform, TranslateTransform etc) hérite de la classe « Transform » ayant pour propriété abstraite :

public abstract Matrix Value { get; }

La classe de base « Transform » propose cette méthode virtuelle :

internal virtual void MultiplyValueByMatrix(ref Matrix result, ref Matrix matrixToMultiplyBy)

{

    result = this.Value;

    MatrixUtil.MultiplyMatrix(ref result, ref matrixToMultiplyBy);

}

Celle-ci n’est pas surchargée pour « RotateTransform » mais comporte une surcharge à des fins d’optimisations pour « TranslateTransform » :

internal override void MultiplyValueByMatrix(ref Matrix result, ref Matrix matrixToMultiplyBy)

{

    result = Matrix.Identity;

    result._offsetX = this.X;

    result._offsetY = this.Y;

    result._type = MatrixTypes.TRANSFORM_IS_TRANSLATION;

    MatrixUtil.MultiplyMatrix(ref result, ref matrixToMultiplyBy);

}

Il en est de même pour la transformation « ScaleTransform » :

internal override void MultiplyValueByMatrix(ref Matrix result, ref Matrix matrixToMultiplyBy)

{

    result = Matrix.Identity;

    result._m11 = this.ScaleX;

    result._m22 = this.ScaleY;

    double centerX = this.CenterX;

    double centerY = this.CenterY;

    result._type = MatrixTypes.TRANSFORM_IS_SCALING;

    if ((centerX != 0.0) || (centerY != 0.0))

    {

        result._offsetX = centerX - (centerX * result._m11);

        result._offsetY = centerY - (centerY * result._m22);

        result._type |= MatrixTypes.TRANSFORM_IS_TRANSLATION;

    }

    MatrixUtil.MultiplyMatrix(ref result, ref matrixToMultiplyBy);

}

On comprend bien que de nombreuses optimisations ont été effectuées par l’équipe WPF de Microsoft afin de réduire les temps de calculs. Toutefois, les choix pratiques, le confort pour l’utilisateur du Framework WPF peut avoir un coût du point de vue de la performance. C’est le fameux dilemme entre facilité de mise en œuvre et performance extrême. Il faut bien avoir conscience que souvent la facilité d’utilisation d’un Framework dépend de compromis. Aussi, si nous revenons sur le sujet de notre article, en particulier sur l’ensemble des transformations que nous souhaiterions mettre en œuvre (rotation de l’objet sur lui-même et translation de l’objet d’un point à un autre), on comprend bien que nous avons un intérêt évident à effectuer les multiplications des matrices sur le papier pour n’avoir qu’une seule matrice au final. D’ailleurs, je vous conseille d’avoir cette approche lorsque vous voulez animer un grand nombre d’éléments dans votre scène WPF. C’est ce que nous allons étudier au prochain chapitre.
Enfin, pour les plus mathématiciens d’entre vous, je vous déconseille d’utiliser systématiquement la « MatrixTransform », surtout si vous avez une simple translation, un changement d’échelle car le type interne de la transformation serait « TRANSFORM_IS_UNKNOWN ». Vous ne pourriez donc pas bénéficier des optimisations du Framework comme nous l’avons vu précédemment.
 
» Démarrer une discussion