Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Animations Lissajous dans Silverlight
Charles Petzold
Nous percevons généralement les logiciels comme plus flexibles et plus polyvalents que le matériel informatique. Cela s'avère fondé dans de nombreux cas, car le matériel est souvent bloqué dans une configuration donnée, alors qu'un logiciel peut être reprogrammé pour effectuer des tâches complètement différentes.
Toutefois, certains composants matériels plutôt prosaïques font en réalité preuve d'une grande polyvalence. Considérons par exemple le célèbre, quoique bien moins courant de nos jours, tube cathodique. Il s'agit d'un dispositif qui émet un flux d'électrons à l'intérieur d'un écran en verre. L'écran est recouvert d'un matériau fluorescent qui réagit, lorsqu'il est sollicité par ces électrons, en émettant brièvement de la lumière.
Dans les téléviseurs et les écrans d'ordinateurs anciens, le canon à électrons se déplace selon un modèle stable, balayant horizontalement de façon répétée la largeur de l'écran tout en parcourant plus lentement l'écran de haut en bas. L'intensité du faisceau d'électrons à un moment donné détermine la luminosité du spot à cet instant. Pour les écrans couleurs, des canons à électrons distincts sont utilisés pour créer les couleurs primaires rouge, verte et bleue.
La direction du canon à électrons est contrôlée par des électro-aimants et le canon peut être dirigé sur tout emplacement arbitraire de la surface en verre à deux dimensions. C'est ainsi que le tube cathodique est utilisé dans un oscilloscope. Le plus souvent, le faisceau balaie horizontalement la largeur de l'écran à une vitesse constante, généralement de façon synchronisée avec une forme d'onde en entrée particulière. La déflexion verticale indique l'amplitude de la forme d'onde à cet instant. La persistance plutôt longue du matériau fluorescent utilisé dans les oscilloscopes permet l'affichage de la forme d'onde complète, en « gelant » la forme d'onde pour permettre son observation.
Les oscilloscopes possèdent également un mode XY qui permet le contrôle de la déflexion horizontale et verticale du canon à électrons par deux entrées indépendantes, généralement des formes d'ondes correspondant à des courbes sinusoïdales. Avec deux courbes sinusoïdales en entrée, à tout instant, le point (x,y) est éclairé, où x et y sont fournis par les équations paramétriques suivantes :
Les valeurs A correspondent aux amplitudes, les valeurs ω aux fréquences et les valeurs k aux déphasages.
Le modèle créé par l'interaction de ces deux ondes sinusoïdales est une courbe de Lissajous, du nom du mathématicien français Jules Antoine Lissajous (1822 - 1880), qui fut le premier à créer visuellement ces courbes en réfléchissant de la lumière entre une paire de miroirs fixés sur des diapasons en vibration.
Vous pouvez expérimenter ce modèle avec un programme Silverlight qui génère des courbes de Lissajous sur mon site Web (charlespetzold.com/silverlight/LissajousCurves/LissajousCurves.html). La figure 1 illustre une courbe standard.
Figure 1 Version Web du programme LissajousCurves
Bien que cela ne soit pas vraiment visible dans une capture d'écran statique, un point vert se déplace sur l'écran gris foncé et laisse derrière lui une trace qui s'estompe après quatre secondes. La position horizontale de ce point est commandée par une courbe sinusoïdale et la position verticale, par une autre. Des modèles récurrents sont obtenus lorsqu'il existe un rapport entier entre les deux fréquences.
Il est maintenant de notoriété publique qu'il convient d'adapter sur Windows Phone 7 tout programme Silverlight, quelle que soit sa qualité, pour révéler d'éventuels problèmes de performances masqués auparavant par la puissance des ordinateurs de bureau. Cette affirmation était certainement vraie pour ce programme et je traiterai de ces problèmes de performances plus loin dans cet article. La figure 2 montre le programme en cours d'exécution sur l'émulateur Windows Phone 7.
Figure 2 Programme LissajousCurves pour Windows Phone 7
Le code téléchargeable est composé d'une solution Visual Studio unique nommée LissajousCurves. L'application Web est composée des projets LissajousCurves et LissajousCurves.Web. L'application Windows Phone 7 a le nom de projet LissajousCurves.Phone. Cette solution contient également deux projets de bibliothèque : Petzold.Oscilloscope.Silverlight et Petzold.Oscilloscope.Phone, mais ces deux projets partagent tous les mêmes fichiers de code.
Envoi ou extraction ?
Outre les contrôles TextBlock et Slider, le seul élément visuel dans ce programme est une classe nommée Oscilloscope qui dérive de UserControl. Les données fournies à Oscilloscope proviennent de deux instances d'une classe nommée SineCurve.
La classe SineCurve elle-même n'a pas de visuels, mais je l'ai dérivée de FrameworkElement de manière à pouvoir placer les deux instances dans l'arborescence visuelle et définir sur elles des liaisons. En fait, tout dans le programme est connecté par des liaisons, des contrôles Slider jusqu'aux éléments SineCurve et de SineCurve jusqu'à Oscilloscope. Le fichier MainPage.xaml.cs pour la version Web du programme n'a pas de code hormis ce qui est fourni par défaut et le fichier équivalent dans l'application téléphonique implémente uniquement la logique de conversion tombstone.
SineCurve définit deux propriétés (assorties de propriétés de dépendance), nommées Frequency et Amplitude. Une instance de SineCurve fournit les valeurs horizontales pour Oscilloscope, tandis qu'une autre instance fournit les valeurs verticales.
La classe SineCurve implémente également une interface que j'ai nommée IProvideAxisValue :
public interface IProvideAxisValue {
double GetAxisValue(DateTime dateTime);
}
SineCurve implémente cette interface avec une méthode assez simple qui fait référence à deux champs, ainsi qu'aux deux propriétés :
public double GetAxisValue(DateTime dateTime) {
phaseAngle += 2 * Math.PI * this.Frequency *
(dateTime - lastDateTime).TotalSeconds;
phaseAngle %= 2 * Math.PI;
lastDateTime = dateTime;
return this.Amplitude * Math.Sin(phaseAngle);
}
La classe Oscilloscope définit deux propriétés (également assorties de propriétés de dépendance) nommées XProvider et YProvider, du type IProvideAxisValue. Afin de créer le mouvement, Oscilloscope installe un gestionnaire pour l'événement CompositionTarget.Rendering. Cet événement est déclenché en synchronisation avec la fréquence de rafraîchissement de l'affichage vidéo et sert ainsi d'outil pratique pour la réalisation d'animations. Lors de chaque appel du gestionnaire CompositionTarget.Rendering, Oscilloscope appelle GetAxisValue sur les deux objets SineCurve définis sur ses propriétés XProvider et YProvider.
En d'autres termes, le programme implémente un modèle d'extraction (pull). L'objet Oscilloscope détermine quand il a besoin de données, puis extrait les données des deux fournisseurs de données. Je traiterai bientôt de la façon dont il affiche ces données.
Lorsque j'ai commencé à ajouter des fonctionnalités au programme (en particulier, deux instances d'un contrôle supplémentaire qui affichaient les sinusoïdes, que j'ai par la suite jugées non instructives et finalement supprimées), j'ai commencé à douter du bien-fondé de ce modèle. J'avais trois objets qui extrayaient les mêmes données de deux fournisseurs et j'ai pensé qu'il serait peut-être plus judicieux d'utiliser un modèle d'envoi (push).
J'ai restructuré le programme afin que la classe SineCurve installe un gestionnaire pour CompositionTarget.Rendering et envoie les données vers le contrôle Oscilloscope par le biais de propriétés à présent nommées simplement X et Y de type double.
J'aurais probablement dû prévoir le défaut primordial de ce modèle d'envoi particulier : l'oscilloscope recevait à présent deux modifications distinctes en X et Y et ne construisait pas une courbe régulière, mais une courbe en forme d'escalier, comme l'illustre la figure 3.
Figure 3 Résultat désastreux de l'expérience d'un modèle d'envoi (push)
Ma décision de revenir au modèle d'extraction fut immédiate !
Rendu avec WriteableBitmap
Lorsque j'ai conçu ce programme, je n'avais absolument aucun doute que l'utilisation de WriteableBitmap constituait la meilleure solution pour implémenter l'écran d'oscilloscope réel.
WriteableBitmap est une image bitmap Silverlight qui prend en charge l'adressage des pixels. Tous les pixels de l'image bitmap sont exposés sous la forme d'un tableau d'entiers 32 bits. Les programmes peuvent obtenir et définir ces pixels de façon arbitraire. WriteableBitmap possède une méthode Render qui permet le rendu des visuels de tout objet du type FrameworkElement dans l'image bitmap.
Si l'oscilloscope avait uniquement besoin d'afficher une courbe statique simple, j'utiliserais Polyline ou Path et n'envisagerais même pas d'utiliser WriteableBitmap. Même si cette courbe avait besoin de changer de forme, Polyline ou Path resterait préférable. Toutefois, la courbe affichée par l'oscilloscope doit croître en taille et doit être colorée de façon irrégulière. La ligne doit s'estomper progressivement : les parties de la ligne affichées depuis peu sont plus lumineuses que les parties plus anciennes. Si j'utilisais une courbe unique, elle devrait présenter des couleurs différentes sur sa longueur. Ce concept n'est pas pris en charge dans Silverlight.
Sans WriteableBitmap, le programme devrait créer plusieurs centaines d'éléments Polyline différents, tous d'une couleur différente et mélangés, et déclenchant des phases de disposition après chaque événement CompositionTarget.Rendering. Mes connaissances de la programmation Silverlight m'indiquaient que WriteableBitmap offrirait sans aucun doute des performances nettement supérieures.
Une version antérieure de la classe Oscilloscope traitait l'événement CompositionTarget.Rendering en obtenant de nouvelles valeurs à partir des deux fournisseurs SineCurve, en les mettant à l'échelle de l'élément WriteableBitmap, puis en créant un objet Line entre le point précédent et le point actuel. Ceci a été simplement transmis à la méthode Render de WriteableBitmap :
writeableBitmap.Render(line, null);
La classe Oscilloscope définit une propriété Persistence qui indique la durée en secondes pendant laquelle la couleur ou le composant alpha quelconque d'un pixel diminue de 255 à 0. Cette technique de fondu impliquait l'adressage direct des pixels. Le code correspondant est reproduit à la figure 4.
Figure 4 Code de décrémentation des valeurs de pixels
accumulatedDecrease += 256 *
(dateTime - lastDateTime).TotalSeconds / Persistence;
int decrease = (int)accumulatedDecrease;
// If integral decrease, sweep through the pixels
if (decrease > 0) {
accumulatedDecrease -= decrease;
for (int index = 0; index <
writeableBitmap.Pixels.Length; index++) {
int pixel = writeableBitmap.Pixels[index];
if (pixel != 0) {
int a = pixel >> 24 & 0xFF;
int r = pixel >> 16 & 0xFF;
int g = pixel >> 8 & 0xFF;
int b = pixel & 0xFF;
a = Math.Max(0, a - decrease);
r = Math.Max(0, r - decrease);
g = Math.Max(0, g - decrease);
b = Math.Max(0, b - decrease);
writeableBitmap.Pixels[index] = a << 24 | r << 16 | g << 8 | b;
}
}
}
À ce stade du développement du programme, j'ai pris les mesures nécessaires pour pouvoir l'exécuter sur le téléphone. Sur le Web ou le téléphone, le programme paraissait s'exécuter correctement, mais je savais qu'il n'était pas encore totalement fini. Je ne voyais pas encore de courbes sur l'écran d'oscilloscope : je voyais une multitude de segments rectilignes joints, or rien ne détruit plus aisément l'illusion d'un phénomène analogique simulé numériquement qu'un ensemble de segments extrêmement droits !
Interpolation
Le gestionnaire CompositionTarget.Rendering est appelé en synchronisation avec le rafraîchissement de l'affichage vidéo. Pour la plupart des affichages vidéo (y compris celui sur Windows Phone 7), le rafraîchissement se situe généralement à environ 60 images par seconde. En d'autres termes, le gestionnaire d'événements CompositionTarget.Rendering est appelé approximativement toutes les 16 ou 17 ms. En fait, comme nous le verrons, il s'agit là de la situation optimale. Même si les ondes sinusoïdales présentent un cycle par seconde, pour un oscilloscope d'une largeur de 480 pixels, deux points d'échantillonnage voisins peuvent avoir des coordonnées éloignées de 35 pixels.
La classe Oscilloscope avait besoin d'effectuer une interpolation à l'aide d'une courbe entre deux points d'échantillonnage consécutifs. Toutefois, quel type de courbe choisir ?
Mon premier choix fut une spline canonique (connue également sous le nom de spline cardinale). Pour une série de points de contrôle p1, p2, p3 et p4, la spline canonique fournit une interpolation cubique entre p2 et p3 avec un degré de courbure basé sur un facteur de « tension ». Il s'agit d'une solution à usage général.
La fonction spline canonique était prise en charge dans Windows Forms, mais n'a pas été retenue dans Windows Presentation Foundation (WPF) ni Silverlight. Heureusement, je disposais d'un code WPF et Silverlight pour la fonction spline canonique que j'avais développée pour une entrée de blog de 2009, judicieusement appelée « Fonctions splines canoniques dans WPF et Silverlight » (bit.ly/bDaWgt).
Après avoir généré un élément Polyline avec interpolation, le traitement de CompositionTarget.Rendering se terminait à présent avec un appel tel que :
writeableBitmap.Render(polyline, null);
La fonction spline canonique fonctionnait, mais quelque chose clochait encore. Lorsque les fréquences des deux sinusoïdes sont des multiples entiers, la courbe doit se stabiliser en modèle fixe. Mais, cela ne se produisait pas et j'ai compris que la courbe interpolée était légèrement différente en fonction des points réellement échantillonnés.
Ce problème était accentué sur le téléphone, en raison principalement de la petite taille du processeur qui peinait à faire face à toutes les demandes que je lui imposais. À des fréquences plus élevées, les courbes de Lissajous obtenues sur le téléphone présentaient des courbures régulières, mais qui semblaient évoluer selon des modèles presque aléatoires !
Je commençais alors à comprendre que je pouvais effectuer l'interpolation en fonction du temps. Deux appels consécutifs du gestionnaire d'événements CompositionTarget.Rendering sont espacés d'environ 17 ms. Je pouvais simplement effectuer une boucle sur toutes ces valeurs en millisecondes intermédiaires et appeler la méthode GetAxisValue dans les deux fournisseurs SineCurve pour créer une polyligne plus régulière.
Cette approche s'avéra nettement plus efficace.
Amélioration des performances
La page de documentation « Considérations liées aux performances dans les applications pour Windows Phone » est un document essentiel pour tous les programmeurs Windows Phone 7. Elle se trouve à l'adresse bit.ly/fdvh7Z. Outre de nombreuses astuces pratiques sur l'amélioration des performances dans vos applications téléphoniques, ce document explique la signification des nombres qui apparaissent sur le bord de l'écran lorsque vous exécutez le programme sous Visual Studio, comme l'illustre la figure 5.
Figure 5 Indicateurs de performances dans Windows Phone 7
Cette ligne de nombres est activée lorsque la valeur true est affectée à la propriété Application.Current.Host.Settings.EnableFrameRateCounter, opération qu'effectue le fichier standard App.xaml.cs si le programme s'exécute sous le débogueur Visual Studio.
Les deux premiers nombres sont les plus significatifs : parfois, si rien ne se passe, ces deux nombres sont nuls, mais ils ont pour but d'afficher les fréquences d'images (ce qui signifie qu'ils représentent un nombre d'images par seconde). J'ai indiqué que la plupart des affichages vidéo sont réactualisés à une fréquence de 60 fois par seconde. Toutefois, un programme d'application peut tenter de réaliser des animations dans lesquelles chaque nouvelle image requiert un temps de traitement supérieur à 16 ou 17 ms.
Par exemple, supposons qu'un gestionnaire CompositionTarget.Rendering requière 50 ms pour effectuer une tâche quelconque. Dans ce cas, le programme effectuera la mise à jour de l'affichage vidéo à une fréquence de 20 fois par seconde. Il s'agit de la fréquence d'images du programme.
Une fréquence de 20 images par seconde n'est pas vraiment une fréquence d'images catastrophique. N'oublions pas que les films sont projetés à 24 images par seconde et qu'une télévision standard a une fréquence d'images réelle (en prenant en compte l'entrelacement) de 30 images par seconde aux États-Unis et de 25 en Europe. Toutefois, lorsque la fréquence d'images descend à 15 ou 10, cela commence à être visible.
Silverlight pour Windows Phone est capable de décharger certaines animations sur l'unité de traitement graphique (GPU, Graphics Processing Unit), afin de disposer d'un thread secondaire (parfois appelé thread de composition ou thread GPU) qui interagit avec l'unité GPU. Le premier nombre correspond à la fréquence d'images associée à ce thread. Le deuxième nombre indique la fréquence d'images de l'interface utilisateur, qui se rapporte au thread principal de l'application. Il s'agit du thread dans lequel tous les gestionnaires CompositionTarget.Rendering s'exécutent.
En exécutant le programme LissajousCurves sur mon téléphone, j'ai noté les nombres 22 et 11, respectivement pour le thread GPU et le thread d'interface utilisateur, et ces nombres ont légèrement diminué lorsque j'ai augmenté la fréquence des sinusoïdes. Comment améliorer ces résultats ?
Je commençais à m'interroger sur le temps que nécessitait l'instruction essentielle suivante dans ma méthode CompositionTarget.Rendering :
writeableBitmap.Render(polyline, null);
Cette instruction aurait dû être appelée 60 fois par seconde avec une polyligne composée de 16 ou 17 segments, mais elle a été appelée en fait 11 fois par seconde avec des polylignes de 90 segments.
Pour mon livre, « Programming Windows Phone 7 » (Microsoft Press, 2010), j'avais écrit une logique de rendu de ligne pour XNA et je suis parvenu à l'adapter pour Silverlight, pour cette classe Oscilloscope. À présent, je n'appelais pas en fait la méthode Render de WriteableBitmap, mais je modifiais directement les pixels dans l'image bitmap pour tracer les polylignes.
Malheureusement, les deux fréquences d'images plongeaient vers zéro ! Cela me suggérait que Silverlight savait comment afficher des lignes dans une image bitmap nettement plus rapidement que moi. Je dois également préciser que mon code n'était pas optimisé pour les polylignes.
À ce stade, je commençais à m'interroger sur la pertinence éventuelle d'une approche autre que WriteableBitmap. J'ai substitué un élément Canvas aux éléments WriteableBitmap et Image, et chaque fois qu'un élément Polyline était créé, je l'ajoutais simplement à l'élément Canvas.
Bien entendu, il n'est pas possible de procéder ainsi indéfiniment. Un élément Canvas avec des centaines de milliers d'enfants n'est pas souhaitable. En outre, ces enfants d'éléments Polyline devaient s'estomper progressivement. J'ai expérimenté deux approches : la première impliquait l'association d'un élément ColorAnimation à chaque Polyline pour réduire le canal alpha de la couleur, puis la suppression de l'élément Polyline dans l'élément Canvas, une fois l'animation terminée. La seconde approche était plus manuelle et consistait à énumérer les enfants des éléments Polyline et à réduire manuellement le canal alpha de la couleur, puis à supprimer l'enfant une fois que le canal alpha était arrivé à zéro.
Ces quatre méthodes existent encore dans la classe Oscilloscope et elles sont activées par quatre instructions #define en haut du fichier C#. La figure 6 indique les fréquences d'images obtenues avec chaque approche.
Figure 6 Fréquences d'images pour les quatre méthodes de mise à jour d'Oscilloscope
Thread de composition | Thread d'interface utilisateur | |
WriteableBitmap avec rendu des éléments Polyline | 22 | 11 |
WriteableBitmap avec remplissage manuel des contours | 0 | 0 |
Élément Canvas avec éléments Polyline avec fondu par animation | 20 | 20 |
Élément Canvas avec éléments Polyline avec fondu manuel | 31 | 15 |
La figure 6 témoigne que mon intuition initiale sur WriteableBitmap était fausse. Dans ce cas, il est véritablement préférable de placer de nombreux éléments Polyline dans un élément Canvas. Les deux techniques de fondu sont intéressantes : lorsque le fondu est réalisé par une animation, il se manifeste dans le thread de composition à une fréquence de 20 images par seconde. Lorsqu'il est effectué manuellement, il intervient dans le thread d'interface utilisateur à une fréquence de 15 images par seconde. Toutefois, l'ajout de nouveaux éléments Polyline intervient toujours dans le thread d'interface utilisateur et cette fréquence d'images est de 20 lorsque la logique de fondu est déchargée sur l'unité GPU.
En conclusion, la troisième méthode présente les meilleures performances globales.
En résumé, quelle leçon tirer de tout cela ? Clairement, pour atteindre des performances optimales, il est nécessaire d'expérimenter encore et toujours. Expérimentez des approches différentes et ne vous fiez jamais à vos intuitions initiales.
Charles Petzold contribue depuis longtemps à l'élaboration de MSDN Magazine*. Son nouveau livre, « Programming Windows Phone 7 » (Microsoft Press, 2010) est disponible en téléchargement gratuit à l'adresse bit.ly/cpebookpdf.*
Je remercie notre expert technique d'avoir relu cet article : Jesse Liberty