Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Animazioni Lissajous in Silverlight
Charles Petzold
In genere si ritiene che il software sia più flessibile e versatile rispetto all'hardware. È vero in molti casi, in quanto l'hardware è spesso limitato a una sola configurazione, mentre il software può essere riprogrammato per eseguire attività completamente diverse.
Eppure alcuni comunissimi componenti hardware sono in effetti piuttosto versatili. Basti pensare al popolare (non più ai giorni nostri) tubo catodico. Si tratta di un dispositivo che emette un flusso di elettroni all'interno di uno schermo in vetro. Lo schermo è rivestito da materiale fluorescente che reagisce agli elettroni producendo una breve irradiazione.
Nei vecchi impianti TV e nei monitor di PC il proiettore elettronico si muove secondo uno schema regolare, propagandosi orizzontalmente in modo ripetitivo sullo schermo e viaggiando più lentamente dall'alto verso il basso. L'intensità di elettroni in qualsiasi momento determina la luminosità di un punto in quel preciso istante. Per i monitor a colori vengono utilizzati proiettori elettronici separati per creare i colori primari rosso, verde e blu.
La direzione del proiettore elettronico è controllata da elettromagneti e può essere puntata in qualsiasi posizione arbitraria nella superficie bidimensionale dello schermo. Il tubo catodico viene impiegato in questo modo in un oscilloscopio. Nella maggior parte dei casi, il fascio si propaga orizzontalmente sullo schermo a una velocità costante, in genere sincronizzato con una specifica forma d'onda di input. La deviazione verticale indica l'ampiezza della forma in quel punto. La persistenza prolungata del materiale fluorescente utilizzato negli oscilloscopi consente la visualizzazione dell'intera forma d'onda (in effetti, "congelandola" per poterla analizzare visivamente).
Gli oscilloscopi dispongono inoltre di una modalità X-Y che consente il controllo della deviazione verticale del proiettore elettronico da parte di due input indipendenti, in genere forme di onda quali curve seno. Con due curve seno come input, in qualsiasi punto nel tempo viene illuminato il punto (x, y), dove x e y vengono ricavati dalle equazioni parametriche:
I valori A rappresento ampiezze, i valori ω frequenze e i valori k differenze di fasi.
Il modello risultante dall'interazione di queste due curve seno è una curva Lissajous, dal nome del matematico francese Jules Antoine Lissajous (1822 - 1880) che le ha create visivamente riflettendo la luce tra una coppia di specchi collegati a diapason.
È possibile esercitarsi con un programma Silverlight che genera curve di Lissajous nel mio sito Web (charlespetzold.com/silverlight/LissajousCurves/LissajousCurves.html). Nella Figura 1 è illustrata una visualizzazione tipica.
Figura 1 Versione Web del programma LissajousCurves
Sebbene non sia evidente in una cattura di schermata statica, un punto verde si muove attorno alla schermata grigio scuro, lasciando dietro di sé una traccia che si dissolve dopo quattro secondi. La posizione orizzontale del punto è regolata da una curva seno e la posizione verticale da un'altra. Se le due frequenze sono semplici rapporti integrali, vengono creati modelli ripetitivi.
È ora universalmente riconosciuto che un buon programma Silverlight deve essere trasferito in Windows Phone 7 e quindi rivelare qualsiasi problema di prestazioni precedentemente mascherato da computer desktop di potenza elevata. È certamente il caso del nostro programma. Più avanti in questo articolo verranno descritti questi problemi relativi alle prestazioni. Nella Figura 2 è illustrato il programma in esecuzione nell'emulatore di Windows Phone 7.
Figura 2 Programma LissajousCurves per Windows Phone 7
Il codice che è possibile scaricare contiene un'unica soluzione di Visual Studio denominata LissajousCurves. L'applicazione Web è costituita dai progetti LissajousCurves e LissajousCurves.Web. Il nome di progetto dell'applicazione Windows Phone 7 è LissajousCurves.Phone. La soluzione contiene inoltre due progetti di librerie (Petzold.Oscilloscope.Silverlight e Petzold.Oscilloscope.Phone) che condividono gli stessi file di codice.
Push o pull?
Oltre ai controlli TextBlock e Slider, l'unico elemento visivo di questo programma è una classe denominata Oscilloscope che deriva da UserControl. Due istanze di una classe denominata SineCurve forniscono i dati per Oscilloscope.
SineCurve non presenta elementi visivi, ma ho derivato la classe da FrameworkElement per poter inserire le due istanze nell'albero visuale e definirvi associazioni. In effetti ogni elemento nel programma è connesso ad associazioni, dai controlli Slider agli elementi SineCurve e dalla classe SineCurve alla classe Oscilloscope. Il file MainPage.xaml.cs per la versione Web del programma non dispone di codice ulteriore rispetto a quello predefinito e il file equivalente nell'applicazione telefonica implementa esclusivamente logica Tombstoning.
SineCurve definisce due proprietà (supportate da proprietà di dipendenza) denominate Frequency e Amplitude. Un'istanza di SineCurve fornisce i valori orizzontali per Oscilloscope, l'altra i valori verticali.
La classe SineCurve implementa inoltre un'interfaccia che ho denominato IProvideAxisValue:
public interface IProvideAxisValue {
double GetAxisValue(DateTime dateTime);
}
SineCurve implementa l'interfaccia con un metodo piuttosto semplice che fa riferimento a due campi e alle due proprietà:
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 definisce due proprietà (anch'esse supportate da proprietà di dipendenza) denominate XProvider e YProvider di tipo IProvideAxisValue. Per iniziare, Oscilloscope installa un gestore per l'evento CompositionTarget.Rendering. Questo evento viene attivato in sincronizzazione con la frequenza di aggiornamento dello schermo del video e opera quindi come uno strumento convenzionale per eseguire le animazioni. Ogni volta che viene chiamato il gestore CompositionTarget.Rendering, Oscilloscope chiama GetAxisValue nei due oggetti SineCurve impostati sulle relative proprietà XProvider e YProvider.
In altre parole il programma implementa un modello di pull. L'oggetto Oscilloscope determina il momento in cui necessita di dati e ne eseguirà il pull dai due provider. A breve verrà illustrato il modo in cui vengono visualizzati questi dati.
Man mano che aggiungevo funzionalità al programma (in particolare, due istanze di un ulteriore controllo che visualizzavano le curve seno, ma che ho quindi rimosso in quanto inutile distrazione), ho iniziato a dubitare della validità di questo modello. Tre oggetti effettuavano il pull degli stessi dati da due provider e ho pensato che sarebbe stato più opportuno un modello di push.
Ho ristrutturato il programma per fare in modo che la classe SineCurve installasse un gestore per CompositionTarget.Rendering ed eseguisse il push dei dati al controllo Oscilloscope attraverso proprietà ora denominate semplicemente X e Y di tipo Double.
Probabilmente avrei dovuto prevedere il difetto fondamentale di questo particolare modello push: la classe Oscilloscope riceveva ora due modifiche separate in X e Y e non costruiva una curva lineare, ma una serie di scalini, come indicato nella Figura 3.
Figura 3 Il disastroso risultato di un esperimento con un modello push
Non ho avuto la minima esitazione e sono tornato al modello pull!
Visualizzazione con WriteableBitmap
Dal momento in cui ho ideato questo programma, non dubitavo del fatto che l'utilizzo di WriteableBitmap rappresentasse la soluzione ideale per implementare lo schermo effettivo dell'oscilloscopio.
WriteableBitmap è una bitmap di Silverlight che supporta l'indirizzamento dei pixel. Tutti i pixel della bitmap sono esposti come array di integer a 32 bit. I programmi possono ottenere e impostare questi pixel in modo arbitrario. WriteableBitmap contiene inoltre un metodo Render che consente di visualizzare gli elementi visivi di un oggetto di tipo FrameworkElement nella bitmap.
Se l'elemento Oscilloscope deve visualizzare una semplice curva statica, utilizzerei Polyline o Path ed escluderei a priori WriteableBitmap. Anche se la curva deve cambiare forma, opterei per Polyline o Path. Ma la curva visualizzata deve avere dimensioni superiori ed essere colorata in modo particolare. La linea dovrà poi dissolversi progressivamente: le parti recentemente visualizzate della linea sono più luminose rispetto alle parti meno recenti. Se utilizzassi una curva singola, avrebbe bisogno di vari colori in tutta la sua lunghezza. Questo concetto non è supportato in Silverlight!
Senza WriteableBitmap, il programma dovrebbe creare molte centinaia di elementi Polyline colorati in modo diverso e in varie posizioni e attivare superamenti di layout dopo ogni evento CompositionTarget.Rendering. Le mie conoscenze sulla programmazione di Silverlight indicavano che WriteableBitmap avrebbe consentito di ottenere prestazioni di livello superiore.
Una versione precedente della classe Oscilloscope elaborava l'evento CompositionTarget.Rendering ottenendo nuovi valori dai due provider SineCurve, eseguendone la scalabilità alla dimensione di WriteableBitmap e quindi costruendo un oggetto Line dal punto precedente al punto corrente. Il tutto è stato quindi passato al metodo Render di WriteableBitmap:
writeableBitmap.Render(line, null);
La classe Oscilloscope definisce una proprietà Persistence indicante il numero di secondi per componente alfa o colore di un pixel da ridurre da 255 a 0. Per eseguire la dissolvenza dei pixel era necessario l'indirizzamento diretto. Il codice è illustrato nella Figura 4.
Figura 4 Codice per la dissolvenza dei valori dei pixel
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;
}
}
}
A questo punto dello sviluppo del programma ho eseguito i passaggi necessari per l'esecuzione nel telefono. Sia nel Web che nel telefono, il programma sembrava funzionare senza problemi, ma ero cosciente del fatto che non ero ancora giunto al termine. Non vedevo infatti le curve nella schermata dell'oscilloscopio, ma un gruppo di linee rette collegate. E nulla distrugge l'illusione di dati analogici simulati in digitale come un gruppo di linee assolutamente rette!
Interpolazione
Il gestore CompositionTarget.Rendering viene chiamato in sincronizzazione con l'aggiornamento dello schermo del video. Per la maggior parte degli schermi, incluso quello di Windows Phone 7, il valore medio è di 60 fotogrammi al secondo. In altre parole, il gestore di eventi CompositionTarget.Rendering viene chiamato circa ogni 16 o 17 minuti (ma come vedremo si tratta solo della situazione ottimale). Anche se per le curve seno si tratta di un ciclo al secondo, per un oscilloscopio da 480 pixel due campioni adiacenti potrebbero presentare coordinate distanti di circa 35 pixel.
L'oscilloscopio doveva interpolare tra campioni consecutivi con una curva, ma che tipo di curva?
La mia prima scelta è ricaduta su una spline di tipo cardinal. Per una sequenza di punti di controllo p1, p2, p3 e p4, la spline di tipo cardinal offre un'interpolazione cubica tra p2 e p3 con un grado di curvatura basato su un fattore di "tensione". Si tratta di una soluzione generica.
La spline di tipo cardinal era supportata nei Windows Form, ma non in Windows Presentation Foundation (WPF) o Silverlight. Fortunatamente avevo sviluppato codice WPF e Silverlight per la spline di tipo cardinal in un post del 2009 del mio blog intitolato appunto "Canonical Splines in WPF and Silverlight" (bit.ly/bDaWgt).
Dopo aver generato un elemento Polyline con interpolazione, l'elaborazione di CompositionTarget.Rendering si era conclusa con una chiamata come questa:
writeableBitmap.Render(polyline, null);
La spline di tipo cardinal aveva funzionato, ma non era del tutto corretta. Se le frequenze delle due curve seno sono semplici integrali multipli, la curva dovrebbe stabilizzarsi in un modello fisso. Poiché questa condizione non si verificava, compresi che la curva interpolata variava in minima parte a seconda degli effettivi punti considerati.
Questo problema si amplificava nel telefono, soprattutto a causa delle dimensioni eccessivamente ridotte del processore, che non consentiva di soddisfare tutte le richieste. A frequenze superiori, le curve Lissajous nel telefono apparivano lineari e curve, ma apparentemente in movimento secondo schemi casuali.
Lentamente ho realizzato che avrei potuto interpolare in base al tempo. Tra due chiamate consecutive al gestore per l'evento CompositionTarget.Rendering intercorrono circa 17 minuti. Avrei potuto semplicemente eseguire un ciclo di tutti questi valori di millisecondi intermedi e chiamare il metodo GetAxisValue nei due provider SineCurve per costruire una polilinea lineare.
Questo approccio si è rilevato ottimale.
Miglioramento delle prestazioni
La pagina "Performance Considerations in Applications for Windows Phone" all'indirizzo bit.ly/fdvh7Z rappresenta un documento essenziale per tutti i programmatori di Windows Phone 7. Oltre a fornire utili suggerimenti su come ottimizzare le prestazioni delle applicazioni per il telefono, spiega il significato dei numeri visualizzati al lato dello schermo se si esegue il programma in Visual Studio, come indicato nella Figura 5.
Figura 5 Indicatori delle prestazioni in Windows Phone 7
Questa riga di numeri viene abilitata impostando la proprietà Application.Current.Host.Settings.EnableFrameRateCounter su true tramite il file App.xaml.cs standard (se il programma è in esecuzione nel debug di Visual Studio).
I primi due numeri sono i più importanti: a volte, se non viene eseguita alcuna operazione, i due numeri corrispondono a zero, ma dovrebbero entrambi visualizzare frequenze di fotogrammi, ovvero un numero di fotogrammi al secondo. Come ho già spiegato, la maggior parte degli schermi video viene aggiornata 60 volte al secondo. Un programma applicativo potrebbe tuttavia tentare di eseguire animazioni in cui ogni fotogramma necessita di più di 16 o 17 minuti per l'elaborazione.
Supponiamo ad esempio che il gestore CompositionTarget.Rendering richieda 50 minuti per eseguire un'attività. In questo caso il programma aggiornerà lo schermo video 20 volte al secondo: è questa la frequenza dei fotogrammi del programma.
20 fotogrammi al secondo rappresentano una frequenza accettabile. I film ad esempio scorrono a una frequenza di 24 fotogrammi al secondo e un programma TV standard presenta una frequenza effettiva (tenendo conto dell'interlacciamento) di 30 fotogrammi al secondo negli Stati Uniti e 25 in Europa. Se la frequenza passa da 15 a 10, è tuttavia necessario prestare attenzione.
Silverlight per Windows Phone è in grado di eseguire l'offload di alcune animazioni nella GPU (Graphics Processing Unit) e pertanto dispone di un thread secondario (a volte definito come composizione o thread GPU) che interagisce con la GPU. Il primo numero rappresenta la frequenza dei fotogrammi associata al thread. Il secondo numero è la frequenza dei fotogrammi dell'interfaccia utente, che si riferisce al thread primario dell'applicazione. Si tratta del thread in cui è eseguito qualsiasi gestore CompositionTarget.Rendering.
Eseguendo il programma LissajousCurves nel telefono, visualizzavo i numeri 22 e 11 rispettivamente per i thread relativi alla GPU e all'interfaccia utente. Questi numeri diminuivano leggermente quando aumentavo la frequenza delle curve seno. Potevo fare di meglio?
Inizia a pensare quanto tempo richiedesse questa istruzione cruciale nel metodo CompositionTarget.Rendering:
writeableBitmap.Render(polyline, null);
Questa istruzione avrebbe dovuto essere chiamata 60 volte al secondo con un elemento Polyline composto da 16 o 17 linee, ma veniva invece chiamata 11 volte al secondo con elementi da 90 segmenti.
Per il mio libro "Programming Windows Phone 7" (Microsoft Press, 2010) ho scritto una logica di visualizzazione delle linee per XNA e sono riuscito ad adattarla a questa classe Oscilloscope di Silverlight. A questo punto non chiamavo più il metodo Render di WriteableBitmap, ma modificavo direttamente i pixel nella bitmap per tracciare le polilinee.
Purtroppo entrambe le frequenze sono calate a un valore pari a zero! A questo punto ho realizzato che Silverlight era in grado di visualizzare le linee in una bitmap molto più rapidamente di me (devo inoltre sottolineare che il mio codice non era ottimizzato per le polilinee).
Ho quindi iniziato a valutare un approccio diverso da WriteableBitmap. Ho sostituito un elemento Canvas per l'elemento WriteableBitmap e Image e man mano che costruivo l'elemento Polyline aggiungevo l'elemento Canvas.
Naturalmente non è possibile eseguire in modo indefinito questa operazione: non è opinabile disporre di un elemento Canvas con centinaia di migliaia di elementi figlio. E inoltre questi elementi figlio dovevano essere caratterizzati da dissolvenza. Ho tentato due approcci: nel primo ho associato un elemento ColorAnimation a ogni elemento Polyline per diminuire il canale alfa del colore e quindi ho rimosso l'elemento Polyline dall'elemento Canvas al termine dell'animazione. Nel secondo approccio, più manuale, ho enumerato gli elementi figlio, diminuito manualmente il canale alfa del colore e rimosso l'elemento figlio quando il canale alfa ha raggiunto il valore zero.
Questi quattro metodi sono tuttora presenti nella classe Oscilloscope e sono abilitati con quattro istruzioni #define nella parte iniziale del file C#. Nella Figura 6 sono illustrate le frequenze dei fotogrammi per ogni approccio.
Figura 6 Frequenze di fotogrammi per i quattro metodi di aggiornamento della classe Oscilloscope
Thread composizione | Thread interfaccia utente | |
WriteableBitmap con visualizzazione Polyline | 22 | 11 |
WriteableBitmap con riempimenti di struttura manuali | 0 | 0 |
Canvas con Polyline con dissolvenza delle animazioni | 20 | 20 |
Canvas con Polyline con dissolvenza manuale | 31 | 15 |
La Figura 6 mi suggerisce che il mio istinto iniziale rispetto a WriteableBitmap mi aveva ingannato. In questo caso è decisamente meglio inserire un gruppo di elementi Polyline in un elemento Canvas. Le due tecniche di dissolvenza sono interessanti: se eseguita da un'animazione, la dissolvenza si verifica nel thread della composizione a 20 fotogrammi al secondo. Se eseguita manualmente, si verifica nel thread dell'interfaccia utente a 15 fotogrammi al secondo. L'aggiunta di nuovi elementi Polyline si verifica tuttavia sempre nel thread dell'interfaccia utente e la frequenza dei fotogrammi è pari a 20 se viene eseguito l'offload della logica di dissolvenza nella GPU.
In conclusione, il terzo metodo consente di ottenere prestazioni migliori.
Cosa abbiamo imparato oggi? Sicuramente che per ottenere prestazioni ottimali è necessario esercitarsi, tentare approcci diversi e mai, dico mai, fidarsi del proprio istinto.
Charles Petzold collabora da molto tempo con la rivista MSDN Magazine*. Il suo nuovo libro intitolato "Programming Windows Phone 7" (Microsoft Press, 2010) è disponibile gratuitamente per il download all'indirizzo bit.ly/cpebookpdf.*
Un ringraziamento al seguente esperto tecnico per la revisione dell'articolo: Jesse Liberty