Fronteras de la UI

Animaciones de Lissajous en Silverlight

Charles Petzold

Descargar el ejemplo de código

Generalmente imaginamos que el software es más flexible y versátil que el hardware. Sin duda esto es verdad en varios casos, puesto que el hardware se queda con una sola configuración, mientras que el software se puede volver a programar para realizar tareas distintas.

Sin embargo, ciertos hardware más bien antiguos son bastante versátiles. Considere al tubo de rayos catódicos común (o no tan común en estos días) o CRT. Este es un dispositivo que proyecta una transmisión de electrones al interior de una pantalla de vidrio. La pantalla está cubierta con un material fluorescente que reacciona a estos electrones brillando brevemente.

En los televisores y monitores de equipos antiguos, el cañón de electrones se mueve siguiendo un patrón estable, con una barrida horizontal de la pantalla y un desplazamiento desde arriba hacia abajo más lento. La intensidad de los electrones en un momento dado determina el brillo de un punto en dicho momento. En las pantallas de colores, se usan diferentes cañones de electrones para crear los colores primarios: rojo, verde y azul.

El cañón de electrones sigue un curso determinado por electroimanes y se puede apuntar a cualquier ubicación de la superficie bidimensional del cristal. Así se usa un CRT en un osciloscopio. Lo más habitual es que el haz haga un barrido horizontal en la pantalla de forma constante, generalmente en sincronía con una forma de onda de entrada específica. La deflexión vertical muestra la amplitud de la forma de onda en este punto. La persistencia relativamente prolongada del material fluorescente que se usa en los osciloscopios permite que se muestre toda la forma de onda, lo cual “congela” la forma de onda para permitir que sea examinada visualmente.

Los osciloscopios también tienen un modo X-Y que permite controlar la deflexión horizontal y vertical del cañón de electrones con dos entradas independientes, las cuales generalmente son formas de onda como curvas de seno. Con dos curvas de seno como entrada, se ilumina el punto (x, y) en cualquier momento, donde los valores x e y son proporcionados por las ecuaciones paramétricas:

parametric equations

Los valores A son amplitudes, los ω son frecuencias y los k son desviaciones de fase.

El patrón que resulta de estas dos ondas de seno es una curva de Lissajous, que recibió su nombre del matemático francés Jules Antoine Lissajous (1822 – 1880), quien ideó estas curvas al hacer rebotar la luz entre un par de espejos adheridos a diapasones en vibración.

Puede experimentar con un programa Silverlight que genera curvas Lissajous en mi sitio web (charlespetzold.com/silverlight/LissajousCurves/LissajousCurves.html). La figura 1 muestra una visualización habitual.

image: The Web Version of the LissajousCurves Program

Figura 1 La versión web del programa LissajousCurves

Si bien no es fácil de apreciar en una captura de pantalla estática, un punto verde se está moviendo en la pantalla gris oscuro y deja un rastro que se desvanece después de cuatro segundos. La posición horizontal de este punto es determinada por una curva de seno y la posición vertical por otra. Los patrones repetitivos se generan cuando las dos frecuencias son radios integrales simples.

Ahora es una verdad universalmente conocida que cualquier programa Silverlight que se precie se debe portar a Windows Phone 7, lo cual, en consecuencia, revelará cualquier problema antes oculto por los equipos de escritorio de alta potencia. Este fue el caso de este programa. Analizaremos estos problemas de rendimiento posteriormente en este artículo. La figura 2 muestra el programa que se ejecuta en el emulador de Windows 7.

image: The LissajousCurves Program for Windows Phone 7

Figura 2 El programa LissajousCurves para Windows Phone 7

El código dispuesto para descarga es una solución de Visual Studio denominada LissajousCurves. La aplicación web contiene los proyectos LissajousCurves y LissajousCurves.Web. La aplicación de Windows Phone 7 tiene el nombre de proyecto LissajousCurves.Phone. La solución también contiene dos proyectos de biblioteca: Petzold.Oscilloscope.Silverlight y Petzold.Oscilloscope.Phone, pero esos dos proyectos comparten todos los archivos de código.

¿Inserción o extracción?

Fuera de los controles TextBlock y Slider, el único elemento visual adicional del programa es una clase denominada Oscilloscope que se deriva de UserControl. Dos instancias de una clase denominada SineCurve proporcionan los datos para Oscilloscope.

SineCurve no tiene elementos visuales, pero derivé la clase de FrameworkElement, de manera que podría poner ambas instancias en el árbol visual y definir los enlaces en ellos. De hecho, todo lo que está en el programa está conectado con enlaces, desde los controles Slider hasta los elementos SineCurve y desde SineCurve hasta Oscilloscope. El archivo MainPage.xaml.cs de la versión web del programa no tiene código fuera de aquel proporcionado de forma predeterminada y el archivo equivalente de la aplicación de teléfono sólo implementa lógica en extinción.

SineCurve define dos propiedades (respaldadas por propiedades de dependencia) denominadas Frequency y Amplitude. Una instancia SineCurve proporciona los valores horizontales para Oscilloscope y el otro los valores verticales.

La clase SineCurve también implementa una interfaz que denominé IProvideAxisValue:

public interface IProvideAxisValue {
  double GetAxisValue(DateTime dateTime);
}

SineCurve implementa esta interfaz con un método bastante simple que hace referencia a dos campos además de a las dos propiedades:

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 clase Oscilloscope define dos propiedades (que también están apoyadas por propiedades de dependencia) denominadas XProvider e YProvider del tipo IProvideAxisValue. Para poner todo en movimiento, Oscilloscope instala un controlador para el evento CompositionTarget.Rendering. Este evento se activa en sincronía con la tasa de actualización de la visualización de vídeo, por lo que sirve como una herramienta práctica para realizar animaciones. A cada llamada hecha al controlador CompositionTarget.Rendering, Oscilloscope llama a GetAxisValue en los dos objetos SineCurve configurados en las propiedades XProvider y YProvider.

En otras palabras, el programa implementa un modelo de extracción. El objeto Oscilloscope determina cuando necesita datos y después extrae los datos de los dos proveedores de datos. (Cómo muestra estos datos es algo que analizaremos pronto).

A medida que empecé a agregar más características al programa (específicamente dos instancias de un control adicional que mostraba las curvas de seno que eliminé posteriormente al ser una distracción poco lúcida), empecé a dudar de la sagacidad de este modelo. Tenía tres objetos que extraían los mismos datos de dos proveedores y pensé que quizás sería mejor un modelo de inserción.

Reestructuré el programa de manera que la clase SineCurve instalara un controlador para CompositionTarget.Rendering e insertara datos en el control Oscilloscope mediante propiedades ahora denominadas simplemente X e Y de tipo double.

Probablemente debí haber previsto el error basal del modelo de inserción indicado: Oscilloscope ahora recibía dos cambios separados en X e Y, además de que no estaba construyendo una curva pareja, sino que una serie de peldaños, como se muestran en la figura 3.

image: The Disastrous Result of a Push-Model Experiment

Figura 3 El resultado desastroso de un experimento con un modelo de inserción

Fue sencillo tomar la decisión de volver al modelo de extracción.

Representación con WriteableBitmap

Desde que concebí este programa, no tuve absolutamente ninguna reserva acerca de que usar WriteableBitmap era la mejor solución para implementar la pantalla de Oscilloscope real.

WriteableBitmap es un mapa de bits de Silverlight compatible con direccionamiento de píxeles. Todos los píxeles del mapa de bits están expuestos como una matriz de enteros de 32 bits. Los programas pueden obtener y configurar estos píxeles de forma arbitraria. WriteableBitmap también tiene un método Render que permite representar el aspecto visual de cualquier objeto de tipo FrameworkElement en el mapa de bits.

Si Oscilloscope sólo necesita mostrar una curva estática simple, yo usaría Polyline o Path y ni siquiera consideraría WriteableBitmap. Incluso si necesitara un cambio de forma de la curva, Polyline o Path seguirían siendo preferibles. Sin embargo, es necesario que la curva que muestra Oscilloscope se haga mayor y que se coloree de forma inusual. La línea debe difuminarse progresivamente: Las partes de la línea que se muestran recientemente son más brillantes que las partes más antiguas de la línea. Si uso una sola curva, necesitaría varios colores en su extensión. Esto no es compatible con Silverlight.

Sin WriteableBitmap, el programa necesitaría crear varios centenares de elementos Polyline, cada uno de ellos con una coloración diferente y en rotación y que se pase el desencadenamiento de diseño después de cada evento CompositionTarget.Rendering. Todo lo que sé sobre programación en Silverlight indica que WriteableBitmap definitivamente ofrece un rendimiento muy superior.

Una versión antigua de la clase Oscilloscope procesó el evento CompositionTarget.Rendering al obtener nuevos valores para los dos proveedores de SineCurve, lo que redujo la escala de los últimos al tamaño de WriteableBitmap y después construyó un objeto Line desde el punto previo al punto actual. Esto simplemente se pasó por el método Render de WriteableBitmap:

writeableBitmap.Render(line, null);

La clase Oscilloscope define una propiedad Persistence que indica el número de segundos para que cualquier componente alfa o de color de un píxel se reduzca desde 255 a 0. El desvanecimiento de esos píxeles está relacionado con el direccionamiento directo de píxeles. El código se muestra en la figura 4.

Figura 4 Código para difuminar valores de píxeles

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;
    }
  }
}

En este punto del desarrollo del programa, tomé los pasos necesarios para ejecutarlo en el teléfono. El programa pareció ejecutarse bien tanto en la web como en el teléfono, pero sabía que no estaba listo del todo. No veía curvas en la pantalla de Oscilloscope: lo que estaba viendo era un montón de líneas rectas conectadas. ¡Nada destruye la ilusión de simulación digital de la tecnología analógica más rápido que un montón de líneas extremadamente rectas!

Interpolación

El controlador de CompositionTarget.Rendering se llama en sincronía con la actualización de la visualización de vídeo. Para la mayor parte de las visualizaciones de vídeo, lo que incluye la visualización en Windows Phone 7, esto se encuadra cerca de los 60 fotogramas por segundo. En otras palabras, el controlador de eventos CompositionTarget.Rendering se llama aproximadamente cada 16 o 17 ms. (De hecho, como verá, esa es sólo la situación óptima). Incluso si las ondas de seno están configuradas a un relajado ritmo de un ciclo por segundo, en un osciloscopio de 480 píxeles de ancho puede que encuentre muestras adyacentes con coordenadas separadas por unos 35 píxeles.

Oscilloscope necesita interpolar entre muestras consecutivas con una curva. Pero, ¿qué clase de curva?

Mi primera opción fue una spline canónica (también conocida como spline cardinal). Para una secuencia de puntos de control p1, p2, p3 y p4; la spline canónica proporciona una interpolación cúbica entre p2 y p3 con un grado de curvatura basada en un factor de “tensión”. Es una solución para toda clase de propósito.

La spline canónica era compatible con Windows Forms, pero nunca se hizo compatible con Windows Presentation Foundation (WPF) o Silverlight. Afortunadamente, tengo algo de código de WPF y Silverlight para la spline canónica que desarrolle para una publicación de blog el año 2009 llamada muy adecuadamente “Splines canónicas en WPF y Silverlight” (bit.ly/bDaWgt).

Después de generar una Polyline con interpolación, el procesamiento de CompositionTarget.Rendering ahora concluirá con una llamada parecida a esto:

writeableBitmap.Render(polyline, null);

La spline canónica funcionó, pero no era realmente adecuada. Cuando las frecuencias de las dos curvas de seno son múltiplos de enteros simples, la curva debiese estabilizarse en un patrón fijo. Sin embargo, esto no ocurre, y me di cuenta de que la curva interpolada era ligeramente diferente dependiendo de los puntos dispuestos para la muestra. 

Este problema se exacerbó en el teléfono, principalmente debido a que el pequeño procesador del teléfono tenía problemas para mantener el ritmo con todas las peticiones que le exigía. A frecuencias altas, las curvas de Lissajous en el teléfono se veían suaves y curvilíneas, ¡pero parecían moverse de forma casi aleatoria!

Lentamente me di cuenta que podía interpolar basándome en el tiempo. Dos llamadas consecutivas al controlador de evento CompositionTarget.Render tienen una diferencia de 17 ms. Simplemente podría hacer un bucle que recorra todos esos valores intermedios de milisegundos y llamar al método GetAxisValue de los dos proveedores SineCurve para construir una polilínea más suave.

Ese enfoque funcionó mucho mejor.

Mejorar el rendimiento.

Una parte de la lectura esencial para todos los programadores en Windows Phone 7 es la página de documentación “Performance Considerations in Applications for Windows Phone” en bit.ly/fdvh7Z. Fuera de varias sugerencias prácticas acerca de la mejora del rendimiento para aplicaciones de teléfono, le indicará el significado de esos números que se muestran en el costado de la pantalla al ejecutar el programa en Visual Studio, como se muestra en la figura 5.

image: Performance Indicators in Windows Phone 7

Figura 5 Indicadores de rendimiento en Windows Phone 7

Esta fila de números se habilita al configurar la propiedad Application.Current.Host.Settings.EnableFrameRateCounter como verdadera, lo que hace el programa App.xaml.cs estándar si el programa se ejecuta en el depurador de Visual Studio.

Los primeros dos números son los más importantes: algunas veces, si nada está ocurriendo, ambos muestran números cero, pero ambos están pensados para mostrar la velocidad de los fotogramas, lo que significa que muestran un número de fotogramas por segundo. Mencioné que la mayor parte de los vídeos se actualiza a un ritmo de 60 veces por segundo. Sin embargo, una aplicación puede intentar realizar animaciones en las que cada nuevo fotograma necesita más de 16 ó 17 ms de tiempo de procesamiento.

Por ejemplo, imagine que un controlador de CompositionTarget.Rendering necesita 50 ms para hacer cualquier tarea que esté haciendo. En ese caso, el programa actualizará la visualización de vídeo en un ritmo de 20 veces por segundo. Esa es la velocidad de los fotogramas.

Ahora, 20 fotogramas por segundo no es una velocidad de fotograma lenta. Tenga en cuenta que las películas se reproducen a 24 cuadros por segundo y la televisión estándar tiene una velocidad de fotograma (tomando en cuenta el entrelazado) de 30 fotogramas por segundo en los Estados Unidos y 25 en Europa. Sin embargo, una vez que la velocidad de fotograma disminuye a 15 ó 10, empieza a hacerse notorio.

Silverlight para Windows Phone puede descargar algunas animaciones a la unidad de procesamiento de gráficos (GPU), de manera que tiene un subproceso secundario (el cual ocasionalmente denominado como subproceso de composición o de GPU) que interactúa con la GPU. El primer número es la velocidad de fotogramas asociada al subproceso. El segundo número es la velocidad de fotogramas de la UI, la cual hace referencia al subproceso primario de la aplicación. Ese es el subproceso que cualquier controlador CompositionTarget.Rendering ejecutará.

Al ejecutar el programa LissajousCurves en mi teléfono, vi las cifras 22 y 11 para los subprocesos de GPU y UI, respectivamente, y disminuyeron un poco cuando aumenté la frecuencia de las curvas de seno. ¿Podría haberlo hecho mejor?

Me empecé a preguntar cuánto tiempo exigía esta instrucción crucial en el método CompositionTarget.Rendering:

writeableBitmap.Render(polyline, null);

Esta declaración debiera haber sido llamada 60 veces por segundo con una polilínea consistente de 16 ó 17 líneas, pero estaba siendo llamada aproximadamente 11 veces por segundo con polilíneas de 90 segmentos.

Escribí un poco sobre la lógica de representación de líneas en XNA en mi libro “Programming Windows Phone 7” (Microsoft Press, 2010) y pude adaptarlo para Silverlight para la clase Oscilloscope. No estaba llamando al método Render de WriteableBitmap en lo absoluto, sino que, en lugar de eso, estaba alterando directamente a los píxeles en el mapa de bits para dibujar las polilíneas.

Desafortunadamente, ambas velocidades de fotogramas llegaron a cero. Esto me indica que Silverlight sabía cómo representar líneas en un mapa de bits más rápidamente de lo que yo sabía hacerlo. (También debiera hacer notar que mi código no estaba optimizado para la representación de polilíneas).

En este punto, empecé a preguntarme si sería razonable tomar un enfoque distinto de WriteableBitmap. Sustituí un Canvas para el elemento WriteableBitmap e Image; a medida que se construía cada Polyline, la agregué a Canvas.

Por supuesto, no puede hacer esto de forma indefinida. No querrá un Canvas con cientos de miles de elementos secundarios. Además, esos elementos secundarios Polyline necesitan desaparecer. Probé dos enfoques: el primero implicaba adjuntar un ColorAnimation a cada Polyline para disminuir el canal alfa del color y después eliminar la Polyline de Canvas cuando se complete la animación. El segundo era un enfoque mucho más manual de enumeración mediante el elemento secundario Polyline, disminuir manualmente el canal alfa del color de forma manual y eliminar el elemento secundario cuando el canal alfa disminuya a cero.

Estos cuatro métodos siguen existiendo en la clase Oscilloscope y están habilitados con cuatro instrucciones #define en la parte superior del archivo C#. La figura 6 muestra las velocidades de fotogramas de cada enfoque.

Figura 6 Velocidades de fotograma para los cuatro métodos de actualización de Oscilloscope

  Subproceso de composición Subproceso de UI
WriteableBitmap con representación de Polyline 22 11
WriteableBitmap con relleno manual de esquema 0 0
Canvas con Polyline con difuminado de animación 20 20
Canvas con Polyline con difuminado manual 31 15

La figura 6 me indica que mi primera intuición sobre WriteableBitmap estaba errada. En este caso, es realmente mejor poner un montón de elementos Polyline en un Canvas. Las dos técnicas de difuminado son interesantes: al realizar la animación, el difuminado ocurre en el subproceso de composición, a los 20 fotogramas por segundo. Cuando se realiza manualmente, se hace en el subproceso de UI a 15 fotogramas por segundo. Sin embargo, el agregar nuevos elementos Polyline siempre ocurre en el subproceso de UI con una velocidad de 20 fotogramas por segundo cuando la lógica de difuminado se descarga a la GPU.

En conclusión, el tercer método tiene el mejor rendimiento general.

Entonces, ¿qué aprendimos hoy? Claramente, para obtener el mejor rendimiento, es necesario experimentar. Pruebe distintos enfoques y nunca, jamás confíe en sus primeras intuiciones.

Charles Petzold es un contribuidor de larga data de MSDN Magazine*. Su nuevo libro, “Programming Windows Phone 7” (Microsoft Press, 2010) está disponible como una descarga gratuita en bit.ly/cpebookpdf.*

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Jesse Liberty