Design a site like this with WordPress.com
Get started

[¿Truco WPF?] User Control a pantalla completa

No he tenido la oportunidad de realizar muchas aplicaciones con WPF y en un proyecto en el que trabajo nos ha surgido la necesidad de incluir un la funcionalidad de poner un User Control a pantalla completa.

Después de hablar con un compañero que sí tiene más experiencia que yo en estos menesteres me sugirió que siguiera estos pasos:

  1. Crea una nueva ventana (“Window”) que contenga sólo un grid con un nombre, por ejemplo “fullScreenGrid“.
  2. Define una función pública que reciba como parámetro el user control que tu quieres y añade el user control como hijo de ese grid: fullScreenGrid.Children.Add( tuUserControl )
  3. Y a correr.

Con esto en la cabeza, lo he hecho y lo hemos conseguido. Sólo hay que tener en cuenta que ántes de añadirlo a la ventana nueva, hay que eliminar el user control de la ventana principal para que podamos cambiar el user control de padre, ya que si no lo hacemos, nos saltará la excepción InvalidOperationException con el mensaje “El elemento especificado ya es el elemento secundario lógico de otro elemento. Desconéctelo primero

También he tenido que añadir un botón en la ventana que está a pantalla completa para poder salir del modo “Full Screen”. Además, para que la ventana esté ha pantalla completa, hay que setear las propiedades WindowStyle a None y WindowState a Maximized para no mostrar la barra con los botones de maximizar, minimizar y cerrar y para que esté maximizada.

Para que podáis ver cómo funciona os dejo un ejemplo de código con un proyecto completo con todo lo necesario y comentado para que lo veáis más claramente: WPF Full Screen Sample

El resultado después de compilar y ejecutar el ejemplo debe ser algo parecido a esto:

Ventana principal

Y cuando hagamos click en el botón de “Go to full Screen”:

User Contorl a pantalla completa

¿Habéis tenido que hacerlo alguna vez? ¿Cómo lo habéis hecho?

¿Es realmente un truco?

Juan María Laó Ramos.

Usar la API de SkyDrive para Javascript en una página local

Hola a todos, hoy voy a hablaros sobre las API Live que tiene disponible Microsoft y que podemos usar desde nuestras aplicaciones c#, JavaScript, Objective-C yJava, pero además, también está disponible una versión Rest para todo lo que no entre en estos lenguajes. Vamos a centrarnos en la API para SkyDrive y sobre cómo podemos configurarla para usarla en una web que estemos desarrollando en una máquina local.

Hola a todos, hoy voy a hablaros sobre las API Live que tiene disponible Microsoft y que podemos usar desde nuestras aplicaciones c#, JavaScript, Objective-C y Java, pero además, también está disponible una versión Rest para todo lo que no entre en estos lenguajes.

Vamos a centrarnos en la API para SkyDrive y sobre cómo podemos configurarla para usarla en una web que estemos desarrollando en una máquina local.

El único requisito que necesitamos es definirnos una aplicación en el “Live Connect Developer Center“. Es necesario crearnos una nueva App en la sección “My Apps” para obtener un client Id que vamos a necesitar para poder usar la API.

Panel Live Connect

Si accedemos a la sección “SDK Interactivo” podemos irnos al primer ejemplo de la API de Sky Drive del  SDK Interactivo  y ver el código. El código que vamos a ver es código JavaScript, ya que no hay otra forma de verlo de manera interactiva en la web, pero en la documentación sí podemos ver cómo usarla desde C#, Objective-C, Rest y Java.


WL.init({ client_id: clientId, redirect_uri: redirectUri });

WL.login({ "scope": "wl.skydrive" }).then(
    function(response) {
        openFromSkyDrive();
    },
    function(response) {
        log("Failed to authenticate.");
    }
);

function openFromSkyDrive() {
    WL.fileDialog({
        mode: 'open',
        select: 'single'
    }).then(
        function(response) {
            log("The following file is being downloaded:");
            log("");

<pre><code>        var files = response.data.files;
        for (var i = 0; i &amp;lt; files.length; i++) {
            var file = files[i];
            log(file.name);
            WL.download({ &amp;quot;path&amp;quot;: file.id });
        }
    },
    function(errorResponse) {
        log(&amp;quot;WL.fileDialog errorResponse = &amp;quot; + JSON.stringify(errorResponse));
    }
);
</code></pre>

}

function log(message) {
    var child = document.createTextNode(message);
    var parent = document.getElementById('JsOutputDiv') || document.body;
    parent.appendChild(child);
    parent.appendChild(document.createElement(&quot;br&quot;));
}

Si ejecutamos este código se nos abrirá un selector de archivos que lo que nos va a permitir es seleccionar un archivo que tengamos en nuestro directorio de SkyDrive, logarnos si no lo estamos ya, y descargarlo. Este es sólo uno de los ejemplos que podemos probar, hay un montón en los que podemos ver cómo usarla para acceder al contenido, cómo recorrer los directorios, etc.

Sin embargo, y es el problema que quiero resolver y el motivo de este post, si usamos la librería JavaScript en una página web que estamos desarrollando, necesitamos hacer un “truco” para poder verlo funcionar en local.

Si os fijáis bien, en la primera línea de código:

WL.init({ client_id: clientId, redirect_uri: redirectUri });

Vemos, que primero tenemos que inicializar “algo” para poder usarla. Para configurarlo adecuadamente necesitaremos el “client id” que comentaba al principio del post y una “redirect_uri. Pues bien, esa uri es un dominio al que se van a redireccionar las peticiones de la API, que necesitamos configurar en el panel de Live Connect:

Configuración de la aplicación

Esa url debe ser de un dominio público y accesible por la api. Sin embargo, como estamos en desarrollo, nuestra web no está todavía disponible ni la tenemos desplegada en un servidor.

¿Qué podemos hacer?

La solución es bien sencilla, tan sólo tenemos que modificar nuestro etchosts para que el dominio que configuremos en el panel de control de Live Connect sea nuestra propia máquina. De esta manera la API funcionará perfectamente y podremos hacer nuestras pruebas en local antes de publicar nuestra aplicación en un servidor. Por ejemplo podemos definir en nuestro etc/host el dominio:  midominiodeprueba.com 127.0.0.1

Y en la página de configuración de la aplicación pondremos:

Confgurando el Dominio de redirección

A partir de este momento ya podemos usar la API sin tener que estar publicando en un servidor nuestra aplicación y haciendo pruebas en él.

Una vez que tengamos nuestra web lista para desplegar, sólo hay que modificar esos valores para que todo vaya como la seda :).

Juan María Laó Ramos

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

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”