Blog

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, 21 Noviembre, 2011 - 16:50

Opera lanzó hace unas semanas un build especial de su navegador que habilitaba el uso de la función getUserMedia de JavaScript. Esta función permite acceder a la webcam directamente desde JavaScript de forma nativa. He creado una nueva demo de js-aruco, mi detector de marcadores de realidad aumentada, usando esta versión para conseguir que todo el proceso sea 100% JavaScript evitando Flash para la captura de la webcam.


Demo online:
www.inmensia.com/files/aruco/getusermedia/getusermedia.html

Para que la demo funcione hay que realizar dos pasos:

1) Instalar la versión especial del navegador de Opera que se encuentra en el siguiente enlace:
http://labs.opera.com/news/2011/10/19/

2) Permitir el acceso del navegador a la webcam a través de las opciones de configuración a través del siguiente enlace:
opera:config#SecurityPrefs|AllowCameraToCanvasCopy

Por motivos de seguridad, el navegador debería pedir permiso a los usuarios antes de acceder a la webcam, pero este funcionamiento no está todavía implementado. Lo que si está desarrollado es que no se pueda acceder por código. Si se intenta acceder con la función getImageData al contenido del canvas se produce una excepción de seguridad.

Actualizado 28/02/2012: Ahora también se puede ejecutar con Chrome 18 o posterior usando el flag --enable-media-stream en línea de comandos.

Juan Mellado, 17 Septiembre, 2011 - 09:52

Una versión modificada de la demo original escrita por Brandon Jones.

He arreglado un pequeño error en el código original para que las texturas se vean correctamente, ya que con las versiones actuales de Chrome y Firefox apenas se distinguen. Y sobre todo he cambiado los parámetros del shader, para añadir mucho especular y que destaque el normal mapping. Todo el código es JavaScript, el modelo se lee de su formato original en MD5, y se renderiza con WebGL directamente en el navegador.

El modelo 3D es propiedad de id Software.

Temas: Personal
Juan Mellado, 10 Septiembre, 2011 - 10:06

VelitaUna muesca más en el modem para este blog, y ya son seis. Ni muchas ni pocas, simplemente las que son.

Un año este último de bastantes proyectos terminados. Abrir una cuenta en un repositorio público es una de las mejores cosas que he podido hacer. Me ha servido para evitar que el código que suelo escribir en mi tiempo libre acabe perdiéndose en el fondo de algún disco duro. Y sobre todo me ha motivado a limpiarlo y terminarlo, en vez de dejarlo como estaba y pasar a otra cosa en cuanto conseguía mi objetivo.

Proyectos terminados:

- js-lzma: Una versión en JavaScript del algoritmo de descompresión de LZMA.

- js-openctm: Una librería en JavaScript para leer ficheros en formato .CTM

- js-aruco: Un sistema de detección de marcadores para aplicaciones de realidad aumentada escrito totalmente en JavaScript.

- flashcam: Un librería en ActionScript3 para capturar imágenes de una webcam y enviarlas a JavaScript.

- js-javadump: Un parser de ficheros .class de Java escrito en JavaScript. De este proyecto no he escrito nunca en el blog. Es ese tipo de cosas al que me refería antes, que acababan perdiéndose por ahí, y que ahora subo al repositorio.

¿Mucho JavaScript? Pues sí, y parece que la tendencia seguirá siendo la misma. Pero quien sabe, tal vez el próximo año escribamos todos en Dart, ese nuevo lenguaje que ha anunciado Google y del que todavía no se conoce ningún detalle. Son todo especulaciones, pero no estaría mal que anunciasen un lenguaje que cumpliese con el ECMA-262 para ejecutarse directamente en el navegador, pero que suponga una pequeña revolución, como el cambio de AS2 a AS3, con la orientación a objetos, y atraiga a la masa de programadores.

Juan Mellado, 8 Septiembre, 2011 - 16:16

Lo bueno de intentar las cosas por uno mismo es que luego se aprecia mejor el trabajo que realizan los demás. E implementar un simple visor de modelos 3D con alguna sencilla técnica de iluminación hace desarrollar cierto sentido de mirada crítica hacia los pixels. ¡Y te permite encontrar un montón de errores! Veamos un par de ellos que he cometido.

En el primer render que puse obtuve un resultado bastante coherente. Poco más o menos lo que buscaba, una imagen "bonita". Sin embargo, ampliando la imagen y parándome a pensar lo que estaba mostrándose por pantalla, observé algunas zonas un tanto extrañas. Concretamente unas partes donde la textura era completamente negra, que aunque no quedaban mal, intuitivamente me hicieron sospechar de que algo no estaba del todo bien.

WebGL - TransparencyEn la imagen de la izquierda, con el marco rojo, se aprecian bien las zonas negras, sobre todo en el tanga y el brazalete, aunque en general en todos los complementos. Por su parte, en la imagen con el marco verde, está el modelo con un render más correcto. Las zonas negras han desaparecido, sustituyéndose por transparencias que permiten ver lo que hay detrás.

Mi error fue convertir las texturas desde su formato original TGA a JPG, para poder utilizarlas con WebGL, sin tener en cuenta el canal alfa. Esas zonas negras en realidad son regiones con el canal alfa activo, y que yo estaba simplemente ignorando.

La gracia del asunto es que al convertir las textura de nuevo, esta vez con el alfa correcto, el modelo dejó automáticamente de verse bien. El problema es que si dos polígonos se solapan, el que tradicionalmente se dibuja es el más cercano al observador y el otro se ignora. Y si el más cercano es transparente entonces lo que se dibuja es el fondo de la imagen, cuando en realidad debería dibujarse el polígono ignorado para producir el efecto de transparencia.

La solución habitual en este caso es desactivar el "depth test" y activar el "blend", pero aún así es necesario mandar los polígonos ordenados de atrás hacia adelante para que se dibujen correctamente. Eso es bastante costoso en JavaScript, desde el punto del rendimiento, así que de momento lo he descartado, aunque he tenido un poco de suerte con la geometría del modelo y he podido generar una parte de forma correcta para poder comparar con el original.

Otro error que cometí estaba esta vez en el segundo render. De nuevo una imagen que parecía correcta... pero que no lo es en absoluto.
WebGL - Tangent SpaceEn la imagen de la izquierda está el detalle de las rodillas del modelo. Dentro del marco rojo está el render original, donde se observa que la iluminación de las rodilleras no es la misma. Una aparece "excavada" hacia afuera mientras que la otra lo hace hacia dentro. Por su parte, dentro del marco verde, está la imagen de un render más correcto, donde ambas rodilleras presentan la misma iluminación.

El problema está en la dirección de los vectores que se utilizan para calcular la iluminación. Al generarlos no había tenido en cuenta que la malla tiene partes simétricas, como las piernas, que utilizan la misma textura, o lo que es lo mismo, las mismas coordenadas uv sobre ella, pero con una orientación contraria.

La solución más común en este caso es calcular, además de la tangente, la orientación, típicamente un valor de 1 ó -1, y utilizarla para corregir el valor de la bitangente. El problema es que implica tener que pasar un nuevo buffer de atributos de entrada al shader, lo que no me ha convencido mucho, pero me ha permitido generar un render parcial más correcto para comparar.

Todos estos errores son muy básicos y existe mucha información para minimizarlos o eliminarlos completamente en función del objetivo que se pretenda conseguir. ¡Pero hasta que no lo intentas no te das cuenta!