[UNIVERSAL] Win2D, Gráficos acelerados por hardware (2 de 2)

Hola a todos!

Hace unas semanas publiqué la primera entrada sobre Win2D, usando la versión 0.0.5 de Win2D. Hoy está disponible ya una nueva versión, la 0.0.7 con muchas mejoras, bugs arreglados y nuevas características. Si quieres ver una lista detallada de los cambios, la tienes en el blog del equipo. Si no has visitado mi primer artículo, ve a por él para saber como descargar y compilar Win2D.

En este artículo quiero centrarme en los efectos, que no pudimos ver en la entrada anterior. Para darle algo de continuidad, voy a usar el mismo ejemplo de la nave espacial de la primera parte y mejorarla con algunos efectos.

Win2D incluye muchos efectos que podemos aplicar a cualquier cosa dibujada por Win2D. Esto, aunque pueda parecer trivial, es muy importante: Normalmente las librerías de efectos nos permiten aplicar estos a una imagen. Con Win2D podremos aplicarlos a geometría, texto, imágenes e incluso a otros efectos, pudiendo producir resultados increibles encadenando varios efectos. Además, el número de efectos disponible está bastante bién:

  • ArithmeticComposite
  • Atlas
  • Blend
  • Border
  • Brightness
  • ColorMatrix
  • ColorSource
  • Composite
  • Crop
  • DirectionalBlur
  • DisplacementMap
  • DistantDiffuse
  • DistantSpecular
  • DpiCompensation
  • GammaTransfer
  • GaussianBlur
  • HueRotation
  • LinearTransfer
  • LuminanceToAlpha
  • Morphology
  • OpacityMetadata
  • PointDiffuse
  • PointSpecular
  • Premultiply
  • Saturation
  • Scale
  • Shadow
  • SpotDiffuse
  • SpotSpecular
  • Tile
  • Transform2D
  • Transform3D
  • Turbulence
  • UnPremultiply

Como podemos ver, una buena cantidad de efectos. Además la posibilidad de aplicarlos de forma encadenada hace que el número de combinaciones y resultados distintos sea enorme. Pero lo mejor, sin duda, es lo sencillo que resulta usarlos.

Aprovechándonos de todos estos efectos, la apariencia final que le daremos a nuestro ejemplo será algo parecido a esto:

image

Hemos añadido una barra inferior con llamas de colores extraños, para simular una explosión que nos persigue y al mismo tiempo hemos aplicado varios efectos a los planetas de forma que ahora aparecen difuminados y con ciertas partes más iluminadas que otras.

Empecemos por el principio. ¿Como podemos aplicar un efecto, por ejemplo de difuminado, a un círculo o un rectángulo? En primer lugar necesitamos crear una supercifie de dibujo compatible con los efectos, por lo que crearemos una instancia de CanvasRenderTarget en la que dibujaremos la forma que necesitemos:

var drawSurface = new CanvasRenderTarget(baseCanvas, 600f, 800f);
using (var ds = drawSurface.CreateDrawingSession())
{
    ds.Clear(Color.FromArgb(0, 0, 0, 0));
    var circleColor = new CanvasRadialGradientBrush(baseCanvas, Colors.White, Colors.DarkBlue);
    circleColor.Center = new Vector2() { X = 300f, Y = 400f };
    circleColor.RadiusX = 200f;
    circleColor.RadiusY = 200f;
    ds.FillCircle(new Vector2() { X = 300f, Y = 400f }, 200f, circleColor);
}

De la misma forma que vimos ya en el primer artículo, creamos un degradado y pintamos nuestro círculo con él. Pero este código no se está dibujando en pantalla, estamos creando una imagen en la superficie de dibujo de tipo CanvasRenderTarget, que debemos dibujar en la sesión del CanvasControl:

using (var drawSession = args.DrawingSession)
{
    drawSession.Clear(Colors.LightGray);

    var drawSurface = new CanvasRenderTarget(baseCanvas, 600f, 800f);
    using (var ds = drawSurface.CreateDrawingSession())
    {
        ds.Clear(Color.FromArgb(0, 0, 0, 0));
        var circleColor = new CanvasRadialGradientBrush(baseCanvas, Colors.White, Colors.DarkBlue);
        circleColor.Center = new Vector2() { X = 300f, Y = 400f };
        circleColor.RadiusX = 200f;
        circleColor.RadiusY = 200f;
        ds.FillCircle(new Vector2() { X = 300f, Y = 400f }, 200f, circleColor);
    }

    drawSession.DrawImage(drawSurface);
}

Ahora si se dibujará nuestro círculo:

image

Pero no tiene nada especial ¿Donde están esos efectos? Vamos a empezar con ellos, creando una nueva instancia de la clase DirectionalBlurEffect que podemos encontrar en el namespace Microsoft.Graphics.Canvas.Effects:

var directionalBlur = new DirectionalBlurEffect()
{
    BorderMode = EffectBorderMode.Soft,
    Angle = 90,
    BlurAmount = 40,
    Source = drawSurface
};

Las tres prímeras propiedades definen parámetros del efecto: Ángulo de difuminación, cantidad y como tratar los bordes de la imagen generada. El cuarto es el interesante. Todo efecto en Win2D, bueno casi todos, tiene la propiedad Source, donde podemos indicar un ICanvasImage como fuente de datos sobre la que aplicar el efecto. En este caso, es el CanvasRenderTarget donde creamos nuestro círculo anteriormente. Ahora solo tenemos que dibujar el efecto, en vez del CanvasRenderTarget y voila!:

drawSession.DrawImage(directionalBlur, 100f, 100f);

El resultado es nuestro mismo círculo, pero con un difuminado direccional aplicado:

image

Ahora ya hemos visto como aplicar un efecto a nuestros objetos. Con cualquier tipo, ya sea texto, círculos, rectángulos, imágenes… con todo tenemos que hacer la misma operación para aplicar un efecto. Pero lo realmente interesante es que podemos aplicar más de uno. Por ejemplo, podemos añadir un efecto de  transformación de color, usando una matriz de 4×4, a nuestro círculo difuminado. Simplemente tenemos que indicar en la propiedad Source el nuevo efecto:

var directionalBlur = new DirectionalBlurEffect()
{
    BorderMode = EffectBorderMode.Soft,
    Angle = 90,
    BlurAmount = 40,
    Source = new ColorMatrixEffect()
    {
        Source = drawSurface,
        ColorMatrix = new Matrix5x4()
        {
            M11 = 0f, M12 = 0f, M13 = 0f, M14 = 0f,
            M21 = 0f, M22 = 0f, M23 = 0f, M24 = 0f,
            M31 = 1f, M32 = 0f, M33 = 1f, M34 = 0f,
            M41 = 0f, M42 = 0f, M43 = 0f, M44 = 0f,
            M51 = 1f, M52 = 1f, M53 = 0.4f, M54 = 1f
        }
    }
};

Así podemos encadenar tantos efectos como queramos. Los resultados dependerán de nuestra habilidad:

image

Si no necesitamos animar el efecto, lo mejor es que lo creemos de antemano, al preparar los objetos. Calcular y aplicar efectos es costoso aunque esté acelerado por hardware. Debemos recordar que estamos tratando además con Tablets y Smartphones, con una capacidad gráfica mucho más reducida que un PC de escritorio o una consola. En nuestro ejemplo, he añadido a la clase Planet una propiedad de tipo ICanvasImage llamada ImageToDraw y una propiedad bool llamada IsPlanet. Si IsPlanet es true, almaceno en ImageToDraw la superficie de dibujo con los efectos aplicados, lista para ser dibujada:

private ICanvasImage CreateDrawableImageWithEffects(Planet item)
{
    var myBitmap = new CanvasRenderTarget(baseCanvas, (item.BodyRadius * 4.0f) + 50.0f, (item.BodyRadius * 4.0f) + 50.0f);
    using (var ds = myBitmap.CreateDrawingSession())
    {
        ds.Clear(Color.FromArgb(0, 0, 0, 0));
        ds.FillCircle(new Vector2()
                        {
                            X = ((item.BodyRadius * 4.0f) + 50.0f) / 2.0f,
                            Y = ((item.BodyRadius * 4.0f) + 50.0f) / 2.0f
                        },
                        item.BodyRadius, item.BodyColor);
    }

    var blurEffect = new GaussianBlurEffect()
    {
        BlurAmount = 30f,
        BorderMode = EffectBorderMode.Soft,
        Source = new GammaTransferEffect()
        {
            Source = myBitmap,
            AlphaAmplitude = 1f,
            RedAmplitude = 3f,
            BlueAmplitude = 15f,
            GreenAmplitude = 2f,
            RedExponent = 5f,
            GreenExponent = 5f,
            BlueExponent = 10f,
            AlphaExponent = 6f
        }
    };

    return blurEffect;
}

Un caso aparte es el del fuego inferior, que está animado. En este caso, la animación se consigue mediante una matríz de 3×2 para la traslación y otra para el escalado. Lo que vamos a hacer es precrear todo el efecto antes de dibujar, sin indicar las matrices:

private void CreateFlameEffect()
{
    this.morphology = new MorphologyEffect()
    {
        Mode = MorphologyEffectMode.Dilate,
        Width = 12,
        Height = 1
    };

    var colorize = new ColorMatrixEffect()
    {
        Source = new GaussianBlurEffect()
        {
            Source = this.morphology,
            BlurAmount = 3f
        },
        ColorMatrix = new Matrix5x4()
        {
            M11 = 0f, M12 = 0f, M13 = 0f, M14 = 0f,
            M21 = 0f, M22 = 0f, M23 = 0f, M24 = 0f,
            M31 = 1f, M32 = 0f, M33 = 1f, M34 = 0f,
            M41 = 1f, M42 = 1f, M43 = 0f, M44 = 1f,
            M51 = 1f, M52 = 0.5f, M53 = 0f, M54 = 0.5f
        }
    };

    //Generate perlin noise field
    this.flameAnimation = new Transform2DEffect()
    {
        Source = new BorderEffect()
        {
            Source = new TurbulenceEffect()
            {
                Frequency = new Vector2() { X = 0.1f, Y = 0.1f },
                Size = new Vector2() { X = 1500, Y = 80f }
            },
            ExtendX = CanvasEdgeBehavior.Mirror,
            ExtendY = CanvasEdgeBehavior.Mirror
        }
    };

    //Create a displacement map.
    this.flamePosition = new Transform2DEffect()
    {
        Source = new DisplacementMapEffect()
        {
            Source = colorize,
            Displacement = this.flameAnimation,
            Amount = 40f
        }
    };
}

En este código podemos ver varios efectos en uso: Morphology, GaussianBlur, ColorMatrix, Transform2D y DisplacementMap. Todavía usaremos otro más, llamado Composite, que nos permitirá combinar dos efectos sin usar su propiedad Source.

En el momento de dibujar, crearemos el objeto al que aplicar los efectos, estableceremos las matrices de traslación y escalado y crearemos el efecto Composite:

//Draw fire!
//Create the object bitmap to render fire over.
using (this.flameRenderBase = new CanvasRenderTarget(baseCanvas, new Size(Math.Ceiling(baseCanvas.ActualWidth), 50)))
{
    using (var ds = this.flameRenderBase.CreateDrawingSession())
    {
        ds.Clear(Color.FromArgb(255, 0, 255, 0));
        ds.FillRectangle(new Rect(0, 0, Math.Ceiling(baseCanvas.ActualWidth), 50), Color.FromArgb(255, 0, 0, 255));
    }

    this.flameAnimation.TransformMatrix = Matrix3x2.CreateTranslation(this.seed / 1, this.seed);
    this.seed -= 3;
    this.flamePosition.TransformMatrix = Matrix3x2.CreateScale(new Vector2(2.1f, 2.5f),
                                                                new Vector2((float)baseCanvas.ActualWidth / 2, 50f));

    this.composite = new CompositeEffect()
    {
        Inputs = { this.flamePosition, flameRenderBase }
    };

    this.morphology.Source = this.flameRenderBase;

    drawSession.DrawImage(this.composite, (float)0, (float)baseCanvas.ActualHeight 30);
}

Y listo, obtendremos un efecto de fuego animado. Con la variable seed controlamos el movimiento. Al hacerla negativa, verticalmente parecerá que el fuego se mueve hacia arriba, mientras que al volverla positiva, parecerá moverse horizontalmente de izquierda a derecha.

Esto es todo por ahora! Lo más interesante de Win2D es que este pequeño ejemplo que hemos hecho, que dista mucho de ser un juego completo por supuesto, comparte todo el código entre Windows y Windows Phone. Además nos aporta una nueva serie de características gráficas para hacer nuestras aplicaciones XAML mucho más ricas visualmente.

Como siempre, aquí puedes descargarte el proyecto de ejemplo actualizado. Espero que te sea de utilidad.

Un saludo y Happy Coding!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *