Flash

Temas: Flash Stratos
Juan Mellado, 22 Noviembre, 2011 - 22:51

WebGL Cheat SheetEl lanzamiento de la versión definitiva de Flash 11 hace unas pocas semanas trajo consigo el tan esperado Stage3D, anteriormente conocido como Molehill. O lo que es lo mismo, la posibilidad de aprovechar las capacidades de las GPUs desde Flash. En principio es una alternativa más a otras opciones disponibles actualmente como WebGL, aunque el anuncio por parte de Adobe de renunciar a seguir desarrollando Flash para móviles en favor de su plataforma AIR nos ha dejado despistados a un montón de gente. Pero decisiones empresariales aparte, el API está ahí fuera y es hora de echarle un vistazo.

Siguiendo mi costumbre, he elaborado una pequeña cheat sheet para que me sirva de referencia, igual que la que hice para WebGL.

Lo primero que llama la atención es que es una API de muy bajo nivel. Los que hayan utilizado alguna vez OpenGL, WebGL o DirectX se sentirán cómodos. Pero sinceramente, me esperaba una jerarquía de clases que ofreciera una funcionalidad de más alto nivel para la gestión de escenas, cámaras, colisiones o carga de modelos. Pero no, las clases que hay son para el acceso a bastante bajo nivel. De hecho, ni siquiera han incluido en el runtime el compilador de shaders.

Pero vayamos por partes, las clases básicas de entrada al API son Stage3D y Context3D. La primera clase representa la superficie sobre la que se dibuja, de forma similar al Stage de toda la vida, y que permite además instanciar la segunda clase a través de un evento (para las situaciones de pérdida de contexto). El contexto 3D es el que permite instanciar al resto de clases como los buffers de índices, vértices, texturas o shaders.

public class Example extends Sprite{
  private var stage3D:Stage3D;
  private var context3D:Context3D;

  public function Example(){
    stage3D = this.stage.stage3Ds[0];
    stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
    stage3D.requestContext3D(Context3DRenderMode.AUTO);
  }

  private function onContext3DCreated(event:Event):void{
    context3D = Stage3D(event.target).context3D;
  }
...

Context3D es la clase base, muy similar a otras APIs, y permite, entre cosas, establecer el estado del render con las clásicas opciones de configuración del z-buffer, blending o stencil, y el envío al driver de la orden de dibujado de triángulos.

Otras clases básicas son VertexBuffer3D, IndexBuffer3D, Texture, CubeTexture y Program3D. Pero tienen muy poca funcionalidad, apenas dos o tres métodos para inicializar su contenido desde fuentes de distintos tipos como arrays o matrices. El resto de clases son básicamente enumerados con las constantes típicas como Context3DTextureFormat, Context3DVertexBufferFormat, Context3DBlendFactor o Context3DStencilAction. En total son unas nueve clases de este último tipo, pero no ofrecen mayor funcionalidad aparte de exponer las constantes.

var triangles:Vector.<uint> = Vector.<uint>( [ ... ] );
...
indexList = context3D.createIndexBuffer(triangles.length);
indexList.uploadFromVector(triangles, 0, triangles.length);
...           
context3D.setCulling(Context3DTriangleFace.BACK);
...

Por lo que respecta a los shaders, Adobe ha definido un lenguaje propio (otro más) de muy bajo nivel llamado AGAL (Adobe Graphics Assembly Language). Es realmente ensamblador, por lo que las operaciones son de muy bajo nivel. Las instrucciones son opcodes que manipulan directamente los distintos tipos de registros habituales (attribute, constant, temporary, output, varying, sampler).

private const VERTEX_SHADER:String =
  "mov v0, va1"; //Color (v0: varying 0 <= va1: attribute 1)
  "m44 op, va0, vc0 \n" + //Perspectiva (op: output position, vc0: constant 0)
       
private const FRAGMENT_SHADER:String =
  "mov oc, v0"; //oc: output color

El array de bytecodes correspondiente a un shader puede generarse en tiempo de diseño mediante Pixel Bender 3D, que es una ampliación de la versión 2D de Pixel Bender, una tecnología de Adobe para el procesamiento óptimo de imágenes y vídeos independiente de la plataforma hardware utilizada basada en el uso de ficheros XML. Aunque afortunadamente los shaders también pueden compilarse de una forma más conveniente en tiempo de ejecución mediante una clase de utilidad externa llamada AGALMiniAssembler.

private var vertexAssembly:AGALMiniAssembler = new AGALMiniAssembler();
private var fragmentAssembly:AGALMiniAssembler = new AGALMiniAssembler();
private var programPair:Program3D;
...
vertexAssembly.assemble(Context3DProgramType.VERTEX, VERTEX_SHADER, false);
fragmentAssembly.assemble(Context3DProgramType.FRAGMENT, FRAGMENT_SHADER, false);
...
programPair = renderContext.createProgram();
programPair.upload(vertexAssembly.agalcode, fragmentAssembly.agalcode);

Por lo que respecta al bucle principal habitual de Flash, no parece requerir modificación, pudiéndose seguir utilizando el evento ENTER_FRAME por ejemplo.

  ...
  this.stage.addEventListener(Event.ENTER_FRAME, render);
}

private function render(event:Event):void{
  ...
  context3D.drawTriangles(indexList, 0, 12);
  context3D.present();
}

En definitiva, una API más, una opción más a tener en cuenta a la hora de representar 3D en el navegador. Quizás de muy bajo nivel para el concepto que suele tenerse en la cabeza de Flash, percepción que la aparición de ActionScript 3 empezó a modificar y que Stage3D no hace más que confirmar.

Para terminar, revisando la web de Flash, he encontrado un enlace a un proyecto de la propia Adobe llamado Proscenium. Una librería gráfica de alto nivel, a modo de engine 3D, que están desarrollando sobre Stage3D. El inconveniente es que está aún en fase de desarrollo y no tiene soporte, aunque ya permite crear primitivas básicas, cargar modelos de objetos, e incluso gestionar colisiones. No sé como acabará, pero supongo que pretenderá ser una alternativa a motores desarrollados de forma independiente, como por ejemplo el popular Away3D y otros, que la propia Adobe parece estar apoyando.

Juan Mellado, 11 Junio, 2011 - 14:30

Después de unas cortas, pero merecidas vacaciones, he retomado el proyecto de realidad aumentada para darle los últimos toques finales. En la lista del TODO me quedaron unas cuantas cosas por hacer que creía importante revisar. Al final he conseguido quitarme las más grandes de en medio, aunque alguna de ellas me han dado algún que otro dolor de cabeza.

Memoria
Lo primero era disminuir la reserva de memoria dinámica y reutilizar buffers. Esto ha sido sencillo, ha bastado con hacer una única reserva de memoria al principio, y luego reutilizar esa misma memoria reservada una y otra vez. La sorpresa desagradable vino cuando me fijé en que el monitor de memoria de Chrome me detectaba un "memory leak" enorme.

Chrome - Memory leak

Como se observa en la primera imagen, la memoria empieza con unos valores iniciales aceptables. No obstante, el problema viene cuando a partir de ahí la memoria reservada no hace más que crecer y crecer, hasta que al alcanzar el límite de 2GB se cuelga la pestaña del navegador. Un error que ya había detectado antes, y que me tenía algo mosqueado.

Chrome - Memory leak

En la segunda imagen se ve la memoria a punto de alcanzar el máximo y provocar que el proceso aborte. No obstante, en esa imagen hay algo aún más importante en lo que fijarse. Y es en el hecho de que la memoria reservada por JavaScript y Flash sigue siendo del mismo orden de magnitud que al principio. La buena noticia es que todo apunta a que el problema no lo está generando el programa, ya que ni JavaScript ni Flash aparentan ser los culpables. Y la mala es que entonces surge la duda: ¿quién diablos está reservando toda esa memoria?

Después de revisar todo una y otra vez, al final me he dado cuenta de que la memoria sólo crece de esa forma cuando tengo abierta la ventana de depuración de Chrome. O sea, que el problema es del navegador, no mía (\o/). Cuando tengo abierta la ventana de depuración (lo que en mi caso es prácticamente el 100% de las veces) la memoria no hace más que crecer y crecer. Pero cuando la tengo cerrada, haciendo una navegación normal, la memoria mantiene sus valores iniciales. ¡Aclarado el misterio!

Rendimiento
El otro gran tema pendiente era el rendimiento de la librería. Y la única forma de sacar conclusiones a este respecto era tomar tiempos.

Chrome - PerformanceEn la imagen de la izquierda puede verse los tiempos de ejecución para cada iteración de la librería. Es decir, cada vez que analiza un fotograma a la búsqueda de marcadores. Como se observa, los tiempos eran bastante estables (una característica de ejecución que he observado en Chrome). Pero eso sí, lamentables.

Mi webcam, la más barata que encontré, es capaz de tomar imágenes a un ritmo de 15 fotogramas por segundo. Lo que da un tiempo de proceso de unos 67 (= 1000 / 15) milisegundos entre un fotograma y otro, mientras que librería estaba tardando 300 milisegundos por fotograma. La buena noticia es que al desplegar el detalle de los tiempos se observaba que en realidad el proceso de detección de marcadores apenas demoraba 40 milisegundos. Así que volvía a surgir una nueva pregunta: ¿en qué se estaba yendo el resto del tiempo?

Pues resulta que todo el tiempo se consume en enviar los fotogramas desde Flash a JavaScript. Y por desgracia estoy obligado a utilizar Flash para capturar el vídeo, ya que todavía no puede hacer directamente desde JavaScript. Resulta que el paso de información entre Flash y JavaScript es bastante lento en general por culpa del proceso de serialización. Cuando implementé la librería de captura de vídeo en Flash me limité a guardar la imagen capturada en un Array y pasárselo a JavaScript, pero eso a la larga ha resultado ser muy sencillo de implementar pero muy poco eficiente.

Chrome - PerformanceEn la imagen de la izquierda puede verse los nuevos tiempos después de una serie de optimizaciones, como la de reserva de memoria inicial indicada anteriormente. Como se observa, el tiempo de proceso en cada iteración cae hasta los 100 milisegundos, todavía alejados de los 67 objetivos, aún cuando el tiempo de proceso de la librería ahora se encuentra alrededor de los 25 milisegundos.

Para acelerar el proceso de transferencia de datos entre Flash y JavaScript he realizado un montón de pruebas. Al final, la mejor implementación que he conseguido realizar, en cuanto a rendimiento, ha consistido en convertir todos los pixels de la imagen a cadena de caracteres y concatenarlos en un String separados por comas. Muy tosco, pero efectivo. Para disminuir el tamaño de la cadena de caracteres lo que he hecho es enviar la diferencia de un pixel con respecto al anterior, ya que normalmente tienden a parecerse. Y para reducir un poco más el número de caracteres los he codificado en base 36. Esto último puede sonar raro, pero es que esa es la mayor conversión de base que realizan de forma nativa tanto como Flash como JavaScript. De esta forma he encontrado un equilibrio entre la cantidad de información a intercambiar y el tiempo necesario para procesarla. He subido la clase ActionScript en un nuevo proyecto que he creado llamado flashcam.

Por último, y entrando un poco más en detalle acerca de los tiempos de proceso de la librería, la función que más tarda con diferencia es la que realiza el filtro gaussiano, ya que ella sola se lleva el 75% del tiempo de ejecución. Curioso que al final una función "auxiliar" sea la que más tiempo tarde. Habrá que buscarle una alternativa.

Juan Mellado, 30 Mayo, 2011 - 07:37

He subido a un repositorio público todo el código JavaScript que he ido generando estos últimos días resultado de portar ArUco, una librería para la construcción de aplicaciones de realidad aumentada escrita en C++ utilizando OpenCV. He llamado js-aruco al proyecto, espero que el nombre no me cause problemas, pero me parecía el más adecuado.

En contra de mi costumbre, he creado una pequeña demo, que tiene la particularidad de que es capaz de obtener imágenes de una webcam a través de una pequeña librería que he escrito en Flash, y que es capaz de capturar vídeo y enviar las imágenes a JavaScript. La demo requiere un ordenador bastante rápido, un navegador moderno actualizado (Chrome o Firefox), Flash para la captura de vídeo, y una webcam.

js-aruco

Demo online:
http://inmensia.com/files/aruco/webcam/webcam.html

La demo detecta los marcadores y dibuja un borde rojo alrededor de los mismos, muestra en azul los números identificadores de cada uno de ellos, y destaca las esquinas superiores izquierda con un pequeño cuadro verde para poder hacer un seguimiento de su orientación real con respecto a la cámara.

Los marcadores deben ser matrices de 7x7, con un borde negro de 1 celda de ancho. La matriz más interna de 5x5 puede tener filas con cualquiera de las siguiente combinaciones válidas:
blanco - negro - negro - negro - negro
blanco - negro - blanco - blanco - blanco
negro - blanco - negro - negro - blanco
negro - blanco - blanco - blanco - negro

TODO
- Optimizar. Para capturar imágenes a un mínimo de 20 fps se requiere ser capaz de procesar cada imagen en tan sólo 50 (= 1000/20) milisegundos, algo bastante alejado del rendimiento actual de la librería, incluso para resoluciones pequeñas de 320x240. [Solucionado]
- Reducir el uso de memoria. Hay unas cuantas funciones que reservan memoria que luego no reutilizan ya que vuelven a hacer la misma reserva para cada imagen procesada. [Solucionado]
- Implementar interpolación bilineal en el warp. En ángulos en torno a los 45 grados, para imágenes pequeñas, no se están detectando a veces los marcadores debido a que se forman "dientes de sierra" en la imagen que provocan que falle la cuenta de pixels para diferenciar los cuadrados blancos de los negros. [Implementado]
- Después de varios minutos de funcionamiento la ventana del navegador da un error, aunque aún no sé si porque falla Flash o JavaScript. Posiblemente sea porque falla el mecanismo que he puesto para intercambiar datos entre Flash y JavaScript, o porque el proceso se queda sin memoria. [Solucionado]
- Sustituir Flash por la futura función JavaScript "getUserMedia", un estándar que aún se está definiendo y que espero que los navegadores implementen algún día. [Implementado]

Realidad Aumentada
1. Introducción
2. Escala de grises (Grayscale & Gaussian Blur)
3. Umbralización (Adaptive Thresholding)
4. Detección de contornos (Suzuki)
5. Aproximación a polígonos (Douglas-Peucker)
6. Transformación de perspectiva (Homography & Otsu)
7. Detección de marcadores
8. js-aruco: Realidad Aumentada en JavaScript

Juan Mellado, 31 Mayo, 2010 - 09:08

Flash Apple Android Windows MobileSmokescreen es otro proyecto que trata de reproducir Flash en el navegador a través de un programa escrito completamente en JavaScript utilizando HTML5. En la web del proyecto se pueden ver algunas demos, e incluso un video con las demos ejecutándose sobre un iPad. Muy similar al proyecto Gordon, en bastantes aspectos.

Por el momento no resulta muy espectacular, ya que las películas Flash que requieren más proceso se ejecutan de una forma bastante lenta. Va requerir un poco más de optimización. Para lo que sirven en realidad este tipo de proyectos es para darse cuenta de que muchas cosas que se hacen "por inercia" con Flash se pueden hacer también con otras tecnologías. El principal problema es que no existen herramientas tan sencillas y productivas como Flash para hacerlas. Hay es donde se tienen que poner las pilas los desarrolladores. Y evidentemente falta que Internet Explorer empiece a soportar todas estas tecnologías. Microsoft debería tomar nota para la versión 9 de su navegador, que ya le toca. Silverlight y HTML5 pueden coexistir.

Según la web del proyecto, el código fuente de Smokescreen se liberará dentro de un tiempo. No obstante, a dia de hoy se puede analizar el JavaScript que se ejecuta en la página de las demos: http://smokescreen.us/demos/js/smokescreen.0.1.3-min.js

Leyendo entre líneas, se puede ver que han implementado un parser de ficheros SWF, de igual forma que se observa algunos nombres de clases del core del runtime de Flash para el player. Lo que no parece tener soporte el reproductor es para ActionScript, al menos en su versión 3, aunque teniendo sólo el código "minificado" es bastante complicado de saber a ciencia cierta.

En cualquier caso, si bien estos "experimentos" resultan interesantes, la guerra entre Flash y HTML5 se ha enfriado un poco con la liberación de la versión Flash 10.1 (beta) que ya se puede ejecutar en móviles con Android 2.2 (Froyo). Google lo tiene claro: dar soporte para Flash, y otros ingenios como Unity por ejemplo. Si los usuarios no quieren utilizarlo que no lo hagan, es algo opcional.

En la pasada Google I/O 2010 se pudieron ver bastantes cosas relativas a este tema, incluida una presentación de una aplicación de Adobe, aún en pañales, para generar animaciones directamente en HTML5 de forma similar a como se hace actualmente en Flash.