Lo que no se mide, no se puede mejorar. Lo que no se mejora, se degrada.

Hace algún tiempo que en el podcast de Aporreando Teclados hemos hablado bastante sobre Agile. Y no es raro que caigan en nuestras manos libros como “Actionable Agile Metrics for Predictability” y” When Will It Be Done” de Daniel S. Vacanti.

Spoiler:

El autor nos indica que la lista de tareas realizadas en un proyecto de software es un generador de números aleatorios. Basta con monitorizar dos cosas:

  • El día en que se empieza a trabajar en esa tarea
  • El día en que se da por cerrada esa tarea

A partir de esas dos fechas, puedes obtener el número de días en las que se ha trabajado. Hace notar que si una tarea empieza y termina el mismo día, no significa que se haya tardado en cerrar cero días, sino que ese caso es un día, aunque haya sido una, dos u ocho horas. Con esto podemos obtener una tabla como la del ejemplo:

En la obra se explica que la columna de Días son números totalmente aleatorios con una sutileza: Son aleatorios sí, pero indican lo que ha ocurrido en la realidad. Y eso es lo importante, ya que con esta información se puede saber algo muy importante:

Cycle Time o tiempo en el que se tarda desde que se empieza una tarea hasta que finalmente se termina.

Una imagen vale más que mil palabras

Esta información podemos visualizarla de una forma más amigable, usaremos un gráfico de dispersión (Scatter plot). En el eje horizontal se muestra el devenir de la vida (el tiempo) y en el eje vertical el Cycle Time de todas las tareas:

Se han marcado diferentes percentiles teniendo en cuenta toda la información de lo que ha pasado en realidad: 95, 85, 50. ¿Porqué?

Los Percentiles

Una primera observación es que parece que hay más tareas por encima del percentil 50 que por debajo. Es decir, hay más tareas que tardan en cerrarse más de 10 días que menos. Por tanto, si nuestros sprints son de 2 semanas, es decir de 10 días, ¿tiene sentido ese tamaño de sprint?

Si en ese sprint decimos que vamos a hacer una tarea, da igual los puntos de historia que tenga, tenemos una probabilidad más alta de NO terminarla que de terminarla.

¿Qué acción podemos tomar? Subir el tamaño de los sprints a … 17 días que es lo que nos está indicando el percentil 85 sería una opción.

Con esto, dada cualquier tarea que añadamos al sprint, tendríamos un 85% de probabilidades de terminarla.

Esta decisión de incrementar el tamaño del sprint es cosa de cada uno, y para nada estoy sugiriendo que la regla sea: el tamaño del sprint debe estar en el percentil 85.  Un sprint de 17 días suena raro, muy raro.

Datos más datos

Una segunda observación es que hay dos puntos que están muy lejos del percentil 95. Podríamos decir que son “outliers” u “outsiders” y no hacerles mucho caso, pero eso es un error. Por muy alejados que estemos del percentil 95, esos puntos son hechos reales que han ocurrido con esas tareas. Y ya en los libros nos adelanta que las predicciones serán tan buenas como buenos sean los datos de los que partimos, si quitamos esos outsiders, quitamos bastante información.

¿Puedes imaginar una predicción meteorológica en la que no se tenga en cuenta el tiempo que hizo ayer? De hecho, es un ejemplo con el que parte el segundo libro, When Will It Be Done, y es que es necesario conocer toda la información anterior para poder hacer una predicción.

Pero una predicción no es lo que va a ocurrir, sino lo que va a ocurrir con cierta probabilidad.

Simulando que es gerundio

El libro no va sobre cómo hacer simulaciones, eso ya se estudia y ha demostrado su valía en la predicción meteorológica e incluso en los experimentos realizados en Los Alamos mientras se creaba la primera bomba nuclear. El sistema que se usa es “Simulación Monte Carlo” pero este humilde blog tampoco se va a centrar en cómo se hace, ya que lo que nos importa es la información que esas simulaciones nos van a proporcionar.

Para los datos del ejemplo, esta simulación, iterada 10.000 veces, para intentar predecir cuándo se terminarán 20 tareas más nos da el siguiente resultado:

  • Con un 50% de probabilidad, las 20 tareas estarán el 14 de Marzo de 2024
  • Con un 70% de probabilidad, las 20 tareas estarán el 2 de Abril del 2024
  • Con un 85% de probabilidad, las 20 tareas estarán el 21 de Abril del 2024
  • Con un 95% de probabilidad, las 20 tareas estarán el 16 de Mayo del 2024

Curiosidad

Fijaos que no hemos hablado de tamaño de tareas ni nada por el estilo; por tanto, da igual si están estimadas o no esas tareas, la realidad al final siempre se acaba imponiendo, y parece que no tiene sentido estimar las tareas, ojo, para un proyecto en marcha, con varios sprints a sus espaldas.

Aunque parezca mentira, no hace falta usar mucha información para empezar a hacer estas simulaciones, tendría que leer otra vez los libros, pero me resuena en la cabeza que con 11 mediciones de tareas que hayas realizado suele ser suficiente para empezar.

A medida que vayamos avanzando en nuestro proyecto, cerrando tareas, estas simulaciones deben ir rehaciéndose. Como la predicción del huracán que decía al principio, cada día que pasa hay que ir actualizando la predicción.

Ganando pasta

Haciendo de décimo hombre, y criticar por criticar, los libros parecen un panfleto publicitario de la herramienta que usan para hacer estos cálculos, que tenéis disponible en https://actionableagile.com/ (Las capturas de pantalla y las simulaciones que he hecho se han sacado de ahí con el plan free)

Los libros y este post son una lista de puntos de porqué debemos empezar a medir ese cycle time, hacer simulaciones y actuar en consecuencia con la información real que tenemos lo antes posible.

Espero que os sirva.

El foreach puede causar problemas de memoria

Después de tener algo de tiempo he ido a mi lista de cosas por leer y me quedado a cuadros cuando lo he leído.

http://blogs.msdn.com/b/etayrien/archive/2007/03/17/foreach-garbage-and-the-clr-profiler.aspx

Sí es un enlace del 2007, lo sé, imaginaos la de cosas que tengo en esa lista :P.

En resumen, imaginemos este código:


class Program
{
    class GameEntity
    {
        public void Update()
        {
        }
    }

<pre><code>static GameEntity[] entities = new GameEntity[100];
static Program()
{
    for (int i = 0; i &amp;lt; entities.Length; i++)
    {
        entities[i] = new GameEntity();
    }
}

static void Main(string[] args)
{
    byte[] byteArray = new byte[1];
    for (int i = 0; i &amp;lt; entities.Length; i++)
    {
        entities[i].Update();
    }
}
</code></pre>

}

En el post original, después de pasar el CLR Profiler no se hace reserva de memoria para un enumerador para poder recorrer la colección, algo lógico. Después, para ver la diferencia con el foreach sustituye el código del for por este otro:

static void Main(string[] args)
{
   byte[] byteArray = new byte[1];
   foreach (GameEntity e in entities)
   {
      e.Update();
   }
}

En el caso del foreach se reserva memoria para un enumerador necesario para recorrer el foreach.

Y todo va perfecto, sin embargo, hay un escenario en el que se pueden producir fugas de memoria:

const int NumEntities = 100;

static List list = new List();
static Program()
{
   for (int i = 0; i < NumEntities; i++)
   {
         list.Add(new GameEntity());
     }
}
static void Main(string[] args)
 {
     UpdateIEnumerable(list);
 }
private static void UpdateIEnumerable(IEnumerable enumerable)
 {
     foreach (GameEntity e in enumerable)
     {
         e.Update();
     }
 }

En este caso sí se producen fugas de memoria. Y es que aunque estemos haciendo un foreach en una lista, cuando se le hace un casting a una interfaz, al tipo valor del enumerador se le hace un box, y se coloca en el heap.

La conclusión:

  • Cuando hacemos un foreach sobre un Collection<T> se reserva memoria para un enumerador.
  • Cuando hacemos un foreach sobre la mayoría de las colecciones, como arrays, listas, colas, listas enlazadas y otras:
    • Si se usan explícitamente, NO se reserva memoria para un enumerador.
    • Si se usan a través de interfaces, se reserva memoria para un enumerador.

Así que si el consumo de memoria es algo crítico en vuestra aplicación o juego , nunca, nunca, uséis interfaces para recorrer una colección.

 

[Update: Gracias a Bernardo por el comentario]

El problema aparece cuando el “foreach” recorre la colección como si fuera un “IEnumerable”. En este caso se utiliza la implementación explícita de “GetEnumerator()” y se realiza “boxing” del “struct” para devolver un tipo “IEnumerator”.

El “boxing” es una operación costosa y puede llegar a consumir mucha memoria.

P.D: el método “GetEnumerator()” de  “Collection” no devuelve un “struct”. Es de las pocas colecciones que son una excepción.

Espero que os haya gustado tanto como a mi. 🙂

Juan María Laó Ramos.

Segundo camino para alcanzar el Nirvana del Garbage Colector y cuál elegir

En este segundo post sobre la serie de optimizaciones en juegos en el que vamos a ver la segunda forma de optimizar el proceso de recolección de basura, evitando caídas de rendimiento en nuestros juegos.

Latencia adecuada.

El tiempo que consume el proceso de recolección de basura es directamente proporcional a lo complejo que sea nuestro heap. Si el heap está vacío, el recolector no tendrá nada que hacer.

Fijaos que hemos dicho “lo complejo que sea nuestro heap”. La complejidad del heap es una combinación del número de objetos vivos y el número de referencias a objetos que tengamos. En realidad no importa nada el tamaño en bytes que los objetos del heap tengan: lo que realmente importa es el número total de objetos (ya que el recolector debe examinar cada uno de ellos) y el número de referencias a objetos (el recolector debe seguir cada referencia para ver a qué está apuntando cada una).

Cuando medimos la complejidad del heap, un array de 100.000 enteros es poco costosa. Aunque ocupe mucha memoria, el recolector de basura sólo tiene que examinar el objeto una vez, y no tiene que mirar dentro.

100.000 arrays, cada una con un entero dentro será más costoso, ya que el recolector tiene más objetos que examinar.

Un array de 100.000 referencias a objetos es también más costoso, ya que aunque el array es sólo un objeto, el recolector tiene que recorrer todas las referencias para ver si cada objeto que contenga el array tiene que seguir vivo. Aunque el array sólo contenga nulls, el recolector los tiene que recorrer todos para asegurarse.

Aquí tenéis unos consejos para reducir la complejidad del heap:

  • Es mejor algunos objetos grandes antes que muchos objetos pequeños.
  • Mejor tener en el heap  tipos por valor que  referencias.
  • Cuantas menos referencias a objetos mejor.
  • Los arrays de tipos por valor son tus amigos!
  • Considera reemplazar referencias a objetos por manejadores enteros, es decir, en lugar de guardar una referencia la nave que creó la bala, podrías guardar el “fui creado por la nave 23” como un entero directamente.
  • Es prefeible un T[] o List<T> antes que un LinkedList<T> o un Dictionary<K,V>

Los discípulos del camino de la latencia no se preocupan de reservar memoria durante el juego. Pueden llamar a news, causar boxings, y usar delegados anónimos y métodos generadores. No les importa que pase el recolector de basura, ya que su heap es tan simple que el proceso termina muy rápido.

¿Cuál de los dos caminos elijo?

Podemos obtener un mejor rendimiento evitando las reservas de memoria, de manera que el recolector nunca pasará. Esto funciona sin importar lo complejo que sea nuestro heap.

También obtendremos un mejor rendimiento manteniendo nuestro heap simple, el recolector tardará muy rápidamente. Esto funciona aunque reservemos memoria durante el juego.

¡¡Lo que no podemos hacer es mejorar el rendimiento mezclando ambas soluciones!! Se consigue muy poco reservando memoria sólo para la mitad de memoria necesaria y tener un heap de complejidad media. Eso producirá un juego con un tamaño medio cada pocos segundos.

Si tu objetivo es evitar la recolección de basura, debes elegir sólo uno de los caminos y seguirlos hasta el final.

Reinicia tu cabeza

Los programadores nuevos en el mundo de evitar la recolección de basura piensan que pueden mejorar el rendimiento llamando a GC.Collect en momentos específicos.

Casi siempre se equivocan. Forzar la recolección de basura es una receta para confundir al recolector y dañar su rendimiento.

Pero…

En Xbox, el .NET Compact Framework realiza una recolección de basura cada vez que se ha reservado un megabyte.

Supongamos que estamos optimizando nuestro juego para evitar reservar de memoria. Después de un estudio cuidadoso con el CLR Profiler hemos conseguido reducir a 50 bytes por frame las reservas de memoria que hacemos, pero no conseguimos reducirlo más, no hay manera.

Además, digamos que nuestro juego se ejecuta a 60 frames por segundo, y que un nivel típico tarda 2 minutos en completarse. Al final del nivel tendremos reservados sólo 352k, no es suficiente para que el recolector se ejecute. En realidad, nuestro juego puede estar hasta 5 minutos sin tener que recolectar nada, el único momento en el que el jugador notará que está pasando el recolector de basura es si él mismo se dedica a recorrer el universo “perdiendo el tiempo”.

Suena razonable no? Seguramente podríamos vivir con ello.

Pero…

Estaremos reservando mucha memoria mientras se carga el nivel, y esto causará muchas recolecciones. En realidad no es un problema: no está mal que el recolector pase en este momento, ya que a la pantalla de carga no le importa el framerate.

Pero ¿qué pasa si durante la carga se reserva algo menos de un número de megabytes, por ejemplo 24,452k? Después de la última recolección en el mega 23, esta operación de carga reserva muy poco como para lanzar otra recolección, pero lo suficiente como para dejarnos sin espacio. Ahora nuestro juego sólo puede reservar 124k antes de lanzar otra recolección, así que el jugador notará esto sólo cuando lleve 42 segundos en el nivel.

La solución es llamar a GC.Collect al final de cada método de carga. Esto limpiará cualquier recolección que hiciese falta, reseteando el reloj para que tarde más en pasar el recolector.

Juan María Laó Ramos.

Artículo original.

Primer camino para alcanzar el Nirvana del Garbage Colector

En este post vamos a ver una de las técnicas usadas para evitar que el recolector de basura se ejecute en mitad de la partida de un juego, teniendo como consecuencia una pérdida de rendimiento.

Para ponernos en contexto y verlo más claro debemos saber que lo que vamos a ver es realmente importante en el mundo de los videojuegos ya que en este mundo el consumo de memoria y de recursos es algo crucial y hay que optimizarlos al máximo. En este sentido el proceso de recolección de basura es algo importantísimo ya que consume recursos del sistema y si se ejecuta en mitad del juego se puede producir una bajada de rendimiento importante.

En este primer post de la serie vamos a ver una de las formas de evitarlo.

Este es el post original de Shawn Hargreaves:

En este post vimos cómo saber si el recolector de basura de nuestra Xbox está tardando mucho. Vimos que el tiempo que tarda el recolector de basura es el resultado del producto del número de veces que se recolecta por la latencia misma de la recolección.

Esto nos hace pensar en dos formas de mejorar el rendimiento del proceso de recolección de basura. Podemos reducir el número de recolecciones, o podemos reducir la latencia del proceso de recolección. Con que consigamos reducir uno de esos valores se notará una mejora bastante notable en el rendimiento total.

En este post vamos a ver el primer caso:

Elegir una frecuencia de recolección adecuada

La frecuencia con la que se produce la recolección de basura es directamente proporcional al número de veces que reservamos memoria. Si nunca reservamos memoria, el proceso nunca se lanzará.

Para reducir el número de recolecciones, debemos reservar memoria para aquello que vamos a necesitar mientras cargamos los niveles, y evitar reservas de memoria en el tiempo de juego, evitando así que el recolector de basura entre en acción en mitad del juego.

Hay varias formas para evitar la reserva de memoria:

No reserves memoria (jah!)

Es simple, no hagas news de tipos por referencia. Está bien cuando lo hacemos con tipos por valor como Matrix, Vector3, Color y demás.

Cada vez que queramos hacer un new de un tipo por referencia, debemos usar un pool de objetos para reusar sus referencias. Los ejemplos de Particle y Audio 3D de creators.xna.com usan esta técnica, y SwampThingTom escribió sobre un pool genérico y reusable.

No uses clases que reserven memoria por sí mismas.

Cuando añadimos datos a una colección como un List<T> o un Dictionary<K,V>, se reserva memoria cuando se quieren ampliar. Podemos evitarlo usando el constructor existente en el que se indica el tamaño específico de la colección. Usando este constructor hacemos que se reserve la memoria necesaria para toda la colección haciendo que esta no tenga que ampliar ni disminuir la memoria que necesita.

Ten cuidado cuando formatees cadenas. Es difícil manipular cadenas en .NET sin causar reservas de memoria.

Evita que el CLR reserve memoria.

El CLR reserva memoria cuando ocurre el boxing. ¡Evítalos como si de una plaga se tratase! El boxing puede ocurrir por muchas razones, algunas son obvias, otras no tanto, veamos cuándo ocurre el boxing.:

  • Si asignamos un tipo por valor a una variable Object.
  • Si guardamos tipos por valor en una colección no genérica.
  • Si accedemos a tipos por valor a través de una interfaz.
  • Si usamos un enumerado como clave de un diccionario, las operaciones internas del diccionario generan boxings. Podemos evitarlo usando claves enteras (de tipo int), y hacer casting a los valores de nuestro enumerado antes de añadirlas al diccionario.

No dejes que el compilador de C# reserve memoria.

Puedes ser realmente tricky usar delegados (especialmente los que están definidos “inline”) sin hacer que el compilador reserve memoria. Esto es todo un mundo por sí solo, pero en caso de duda evita el uso de delegados o eventos

Los yield hacen que el compilador reserve siempre memoria.

Los foreach pueden reservar memoria sin cuidado. Pero ojo, esto no significa que tengamos que evitarlos. Suele ser la forma más rápida de recorrer una colección.  Aprended las reglas para usarlos de manera adecuada

Todo lo que no reserve memoria está bien.

A los discípulos del camino de la frecuencia se les permite tener estructuras de datos complejos. Pueden reservar cientos de miles de objetos mientras su juego carga, llenar el heap con una malla de objetos referenciados entre si. Siempre que no reserven nada después de que termine de cargar, el recolector de basura nunca se ejecutará, así que no hay problema ninguno en mantener en memoria objetos complejos.

Resumen

Hemos visto una forma de hacer que el recolector de basura no se ejecute mientras se está jugando al juego: evitando hacer reservas de memoria durante el juego, en vez de eso las hacemos en el proceso de carga y se mantiene todo en memoria.

En el próximo post veremos la segunda opción: Elegir la latencia adecuada.

Juan María Laó Ramos.

Post original.

La “locura” de las optimizaciones en los juegos.

¿A quien no le preocupa que su código no sea óptimo? Eso de “funcionar funciona … pero tarda mucho” no es escusa para no preocuparse por optimizar las partes optimizables. Una de las mejores formas de evitarse problemas desagradables cuando el sistema está “terminado” es interiorizar los conceptos y aplicarlos en la práctica de manera casi automática.

Para ponernos en contexto y verlo más claro debemos saber que lo que vamos a ver es realmente importante en el mundo de los videojuegos ya que en este mundo el consumo de memoria y de recursos es algo crucial y hay que optimizarlos al máximo. En este sentido el proceso de recolección de basura es algo importantísimo ya que consume recursos del sistema y si se ejecuta en mitad del juego se puede producir una bajada de rendimiento importante.

Estos conceptos no son sencillos, ya que involucran “gran cantidad” de detalles que se suelen pasar por alto.

En un afán de mejorar y compartir lo aprendido vamos a ver en detalle un par de posts de  Shawn Hargreaves que he encontrado y que publicaré en varios posts para no saturar y podamos interiorizarlos mejor.

Espero que os sirva.

Juan María Laó Ramos

CodeFluent Entities. Nunca el DDD fué tan fácil

Vamos a ver en este post es un producto que he encontrado y parece muy prometedor. CodeFluent Entities es una herramienta de modelado que nos permite generar y mantener actualizadas todas las layers y capas de nuestra aplicación. Asegurando el conjunto de buenas prácticas arquitecturales en nuestros sistemas desde su nacimiento. Esto es, han creado una herramienta para aplicar Domain Driven Development sin que apenas nos demos cuenta.

La he estado probando un tiempo y la verdad me ha dejado sorprendido, pensé en un momento que iba a ser un ORM más, pero luego descubrí el modelador de objetos de negocio, seguido de la generación de servicios RESTful, la sencillez de enlazar el modelo de negocio con las interfaces de usuario ASP.NET Web Forms, WPF y Windows Forms (están trabajando en los conectores para ASP.NET MVC, Silverlight). Continue reading “CodeFluent Entities. Nunca el DDD fué tan fácil”

¡¡¡Normalízate!!!

¿Porqué respiramos? Respirar es algo normal que hacemos sin pensar. Parpadeamos sin darnos cuenta. Pues lo mismo debería pasar cuando vas a crear una tabla en una base de datos. ¿Cual es el primer campo que se añade a toda tabla? … Si NO has pensado  una Clave Primaria auto numérica y auto incrementable, mejor será que esfuerces en no dejar de respirar. Continue reading “¡¡¡Normalízate!!!”

El antipatrón “desenrollando switchs”

Las técnicas de desenrollado de bulces las usan los compiladores para optimizar el pipeline de los procesadores para aprovechar los huecos de las unidades funcionales disponibles y evitar que éstas estén ociosas y aumentando así el paralelismo del código consiguiendo de esta forma reducir el CPI.

Sin embargo nos empeñamos en una y otra vez todo ese conocimiento con código como este:

enum Axis
{
    XAxis,
    YAxis,
    ZAxis,
};
// code earlier in the function ensure that
// "axis" is always a valid axis
int newPosition;
switch (axis)
{
case XAxis:
     newPosition = m_position[XAxis] + amount;
     if (newPosition < m_minPosition[XAxis])
         newPosition = m_minPosition[XAxis];
     if (newPosition > m_maxPosition[XAxis])
         newPosition = m_maxPosition[XAxis];
     m_position[XAxis] = amount;
break;
case YAxis:
     newPosition = m_position[YAxis] + amount;
     if (newPosition < m_minPosition[YAxis])
         newPosition = m_minPosition[YAxis];
     if (newPosition > m_maxPosition[YAxis])
         newPosition = m_maxPosition[YAxis];
     m_position[YAxis] = amount;
break;
case ZAxis:
     newPosition = m_position[ZAxis] + amount;
     if (newPosition < m_minPosition[ZAxis])
        newPosition = m_minPosition[ZAxis];
     if (newPosition > m_maxPosition[ZAxis])
        newPosition = m_maxPosition[ZAxis];
     m_position[ZAxis] = amount;
break;
}

(me pregunto cuantas veces he escrito código así)

Empujados por saber que el código específico es más rápido que el código de propósito general, algunas veces no nos damos cuenta de que este mismo trozo de código se podría haber escrito de esta manera:

newPosition = m_position[axis] + amount;
if (newPosition &lt; m_minPosition[axis])
   newPosition = m_minPosition[axis];
if (newPosition &gt; m_maxPosition[axis])
   newPosition = m_maxPosition[axis];
m_position[axis] = amount;

Lo malo del switch primero que vimos es que no es fácil distinguir rápidamente si es un “error” del típico “copy&paste que queda nada para salir y me tengo que ir a casa” o hay algo escondido y sutil que hace de este código un ejemplo de libro.

Espero que os sirva.

Juan María Laó Ramos

Aliasing en formas geométricas

Aquí tenéis la octava entrega de la serie sobre aliasing.

Aquí tenéis la octava entrega de la serie sobre aliasing

En el post de Multisampling vimos que la GPU “era lenta”, así que el procesado de triángulos también. Las formas curvas complejas se aproximaban por un pequeño número de triángulos, donde cada uno de ellos era lo suficientemente grande como para cubrir varios píxeles de la pantalla.

 Esto creó una situación extraña en los primeros artículos sobre multisampling que se centraban en cómo suavizar los dientes que le salían a los triángulos. Nos acostumbramos tanto a esa forma de ver gráficos que hasta nos parecía hermoso, sin siquiera pararnos a pensar “¡Hey, la cabeza de este tipo sólo tiene 5 triángulos!”

Pero el hardware mejoró. El número de triángulos subió. Un juego moderno usa entre 4000 y 6000 triángulos para un personaje.

Fijaos en la situación:

  • El personaje se pinta de manera que ocupa 1/8 del alto de la pantalla.
  • El juego se renderiza a 720p, así que la imagen resultante es de 90 píxeles de alto.
  • Es decir que se cubren unos 2000 píxeles de la pantalla.
  • Suponiendo que la mitad de los triángulos están detrás de la cara.
  • Tenemos 3000 triángulos en total, cubriendo tan sólo 2000 píxeles de la pantalla.

Con tanto detalle en nuestros modelos, la geometría facial es algo del pasado. En su lugar tenemos un nuevo problema: ¡Hemos sido Nyquistados!

Recordad: para evitar el aliasing debemos tomar al menos dos veces más muestras de salida según la frecuencia más alta de la señal de entrada. Pero resulta que tenemos menos píxeles de salida que triángulos de entrada. Es bastante obvio que esto va a causar aliasing. Con menos píxeles que triángulos, algunos triángulos no aparecerán en la imagen de salida:

Juanma Pixelado

A medida que el objeto se mueva, los triángulos que sean afortunados y tengan un pixel irán cambiando aleatoriamente, así que la imagen se irá distorsionando a medida que los triángulos vayan apareciendo y desapareciendo.

En realidad existe un truco para suavizar el número de triángulos que un modelo debe contener. Para la mejor calidad, querremos que cada triángulo cubra exactamente dos píxeles de pantalla. Si los triángulos son más pequeños que esto, cruzaremos el límite de Nyquist y aparecerá el aliasing. Si son mayores, tu silueta saldrá “poligonizada”.

NO es totalmente intuitivo que el truco sean dos píxeles por triángulo en lugar de uno, pero es verdad. Imaginad un círculo que es aproximado por una serie de segmentos de líneas. A medida que incrementamos el número de segmentos, el círculo se vuelve más y más perfecto. El momento en el que cada segmento va alcanza la longitud de dos píxeles, el círculo se convierte en perfecto. Por lo que añadir más segmentos a partir de aquí no hará ninguna mejora sobre la curva del círculo.

No es práctico tampoco mantener todos los triángulos con el mismo tamaño en la pantalla, ya que nuestro modelo se tiene que pintar a diferentes tamaños a medida que el jugador se mueve por el mundo.

Así que ¿cómo podemos pintar una geometría con muchos detalles sin tener aliasing?

Multisampling (o supersampling si podemos permitírnoslo) nos ayuda a alejar de nosotros el límite de Nyquist, pero sólo con eso no es suficiente para evitar el aliasing en geometrías.

Normalmaps puede ser una técnica poderosa. Tenemos bastantes opciones para evitar el aliasing cuando reconstruimos texturas, de manera que cada vez que podamos tendremos qie eliminar los detalles de nuestra geometría y reemplazarla con un normalmap, que nos ayudará a controlar el aliasing. Solemos pensar que estas técnicas de normalmaps son tan sólo una optimización del rendimiento (reemplazando geometrías caras con una textura) pero también pueden mejorar la calidad visual.

Por último, es importante considerar que el nivel de detalle del modelo puede variar. Cuando el objeto está muy lejos, cambiar un modelo de 6000 triángulos por uno más simple de 1000 o 500 triángulos es una opción que no sólo aumentará el rendimiento, sino que reducirá el efecto de aliasing en geometrías.

Moraleja: cuando hablamos de números de triángulos, más no siempre es lo mejor. Ten cuidado de no pintar más triángulos de los píxeles que ves en pantalla. Ese camino lleva al mundo del aliasing.

Artículo original.

P/D: Síguieme en twitter: @juanlao

Texturas base sin aliasing

Aquí tenéis la octava entrega de la serie sobre aliasing. Quizás sea demasiado obvio mencionarlo, pero aunque apliquemos los filtros bilineales o las técnicas más inteligentes, no servirán de nada si las imágenes originales tienen problemas de aliasing. Continue reading “Texturas base sin aliasing”

Design a site like this with WordPress.com
Get started