Compartir vía


Ruido y composición de SkiaSharp

Los gráficos vectoriales simples tienden a parecer poco naturales. Las líneas rectas, las curvas suaves y los colores sólidos no se asemejan a las imperfecciones de los objetos del mundo real. Al trabajar en los gráficos generados mediante computación para la película Tron de 1982, el científico informático Ken Perlin comenzó a desarrollar algoritmos que usaban procesos aleatorios para proporcionar a estas imágenes unas texturas más realistas. En 1997, Ken Perlin ganó un premio de la Academia por sus logros técnicos. Su trabajo ha llegado a ser conocido como ruido Perlin y es compatible con SkiaSharp. Este es un ejemplo:

Ejemplo de ruido de perlin

Como puede ver, cada píxel no es un valor de color aleatorio. La continuidad del píxel al píxel da como resultado formas aleatorias.

La compatibilidad con el ruido Perlin en Skia se basa en una especificación W3C para CSS y SVG. La sección 8.20 del Módulo de efectos de filtro - Nivel 1 incluye los algoritmos de ruido Perlin subyacentes en el código C.

Exploración del ruido Perlin

La clase SKShader define dos métodos estáticos diferentes para generar ruido Perlin: CreatePerlinNoiseFractalNoise y CreatePerlinNoiseTurbulence. Los parámetros son idénticos:

public static SkiaSharp CreatePerlinNoiseFractalNoise (float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed);

public static SkiaSharp.SKShader CreatePerlinNoiseTurbulence (float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed);

Ambos métodos también existen en versiones sobrecargadas con un parámetro SKPointI adicional. La sección relativa a la Disposición en mosaico del ruido Perlin describe estas sobrecargas.

Los dos argumentos baseFrequency son valores positivos definidos en la documentación de SkiaSharp como comprendidos entre 0 y 1, pero también se pueden establecer en valores superiores. Cuanto mayor sea el valor, mayor será el cambio en la imagen aleatoria en las direcciones horizontales y verticales.

El valor numOctaves es un entero de 1 o superior. Se relaciona con un factor de iteración en los algoritmos. Cada octava adicional contribuye a un efecto que es la mitad de la octava anterior, por lo que el efecto disminuye con valores de octava más altos.

El parámetro seed es el punto de partida del generador de números aleatorios. Aunque se especifica como un valor de separador flotante, la fracción se trunca antes de que se use y 0 es igual que 1.

La página Perlin Noise del ejemplo permite experimentar con varios valores de los argumentos baseFrequency y numOctaves. Este es el archivo XAML:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.PerlinNoisePage"
             Title="Perlin Noise">

    <StackLayout>
        <skia:SKCanvasView x:Name="canvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Slider x:Name="baseFrequencyXSlider"
                Maximum="4"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Label x:Name="baseFrequencyXText"
               HorizontalTextAlignment="Center" />

        <Slider x:Name="baseFrequencyYSlider"
                Maximum="4"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Label x:Name="baseFrequencyYText"
               HorizontalTextAlignment="Center" />

        <StackLayout Orientation="Horizontal"
                     HorizontalOptions="Center"
                     Margin="10">

            <Label Text="{Binding Source={x:Reference octavesStepper},
                                  Path=Value,
                                  StringFormat='Number of Octaves: {0:F0}'}"
                   VerticalOptions="Center" />

            <Stepper x:Name="octavesStepper"
                     Minimum="1"
                     ValueChanged="OnStepperValueChanged" />
        </StackLayout>
    </StackLayout>
</ContentPage>

Usa dos vistas Slider para los dos argumentos baseFrequency. Para expandir el rango de los valores inferiores, los controles deslizantes son logarítmicos. El archivo de código subyacente calcula los argumentos a los métodos SKShader de las potencias de los valores Slider. Las vistas Label muestran los valores calculados:

float baseFreqX = (float)Math.Pow(10, baseFrequencyXSlider.Value - 4);
baseFrequencyXText.Text = String.Format("Base Frequency X = {0:F4}", baseFreqX);

float baseFreqY = (float)Math.Pow(10, baseFrequencyYSlider.Value - 4);
baseFrequencyYText.Text = String.Format("Base Frequency Y = {0:F4}", baseFreqY);

Un valor 1 de Slider corresponde a 0,001, un valor 2 de Slider corresponde a 0,01, un valor 3 de Slider corresponde a 0,1 y un valor 4 de Slider corresponde a 1.

Este es el archivo de código subyacente que incluye ese código:

public partial class PerlinNoisePage : ContentPage
{
    public PerlinNoisePage()
    {
        InitializeComponent();
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnStepperValueChanged(object sender, ValueChangedEventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Get values from sliders and stepper
        float baseFreqX = (float)Math.Pow(10, baseFrequencyXSlider.Value - 4);
        baseFrequencyXText.Text = String.Format("Base Frequency X = {0:F4}", baseFreqX);

        float baseFreqY = (float)Math.Pow(10, baseFrequencyYSlider.Value - 4);
        baseFrequencyYText.Text = String.Format("Base Frequency Y = {0:F4}", baseFreqY);

        int numOctaves = (int)octavesStepper.Value;

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader =
                SKShader.CreatePerlinNoiseFractalNoise(baseFreqX,
                                                       baseFreqY,
                                                       numOctaves,
                                                       0);

            SKRect rect = new SKRect(0, 0, info.Width, info.Height / 2);
            canvas.DrawRect(rect, paint);

            paint.Shader =
                SKShader.CreatePerlinNoiseTurbulence(baseFreqX,
                                                     baseFreqY,
                                                     numOctaves,
                                                     0);

            rect = new SKRect(0, info.Height / 2, info.Width, info.Height);
            canvas.DrawRect(rect, paint);
        }
    }
}

Este es el programa que se ejecuta en dispositivos iOS, Android y UWP (Plataforma universal de Windows). El ruido fractal se muestra en la mitad superior del lienzo. El ruido de turbulencia está en la mitad inferior:

Ruido de perlin

Los mismos argumentos siempre generan el mismo patrón que comienza en la esquina superior izquierda. Esta coherencia es obvia cuando se ajusta el ancho y el alto de la ventana de UWP. A medida que Windows 10 vuelve a dibujar la pantalla, el patrón de la mitad superior del lienzo sigue siendo el mismo.

El patrón de ruido incorpora varios grados de transparencia. La transparencia se vuelve obvia si se establece un color en la llamada a canvas.Clear(). Ese color se vuelve destacado en el patrón. También verá este efecto en la sección de Combinación de varios sombreadores.

Estos patrones de ruido Perlin rara vez se usan por sí mismos. A menudo están sujetos a modos de mezcla y filtros de color que se describen en artículos posteriores.

Disposición en mosaico del ruido Perlin

Los dos métodos SKShader estáticos para crear ruido Perlin también existen en versiones de sobrecarga. Las sobrecargas CreatePerlinNoiseFractalNoise y CreatePerlinNoiseTurbulence tienen un parámetro SKPointI adicional:

public static SKShader CreatePerlinNoiseFractalNoise (float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed, SKPointI tileSize);

public static SKShader CreatePerlinNoiseTurbulence (float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed, SKPointI tileSize);

La estructura SKPointI es la versión entera de la estructura SKPoint conocida. SKPointI define propiedades X y Y de tipo int en lugar de float.

Estos métodos crean un patrón de repetición del tamaño especificado. En cada mosaico, el borde derecho es el mismo que el borde izquierdo y el borde superior es el mismo que el borde inferior. Esta característica se muestra en la página de Disposición en mosaico del ruido Perlin. El archivo XAML es similar al ejemplo anterior, pero solo tiene una vista Stepper para cambiar el argumento seed:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.TiledPerlinNoisePage"
             Title="Tiled Perlin Noise">

    <StackLayout>
        <skia:SKCanvasView x:Name="canvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <StackLayout Orientation="Horizontal"
                     HorizontalOptions="Center"
                     Margin="10">

            <Label Text="{Binding Source={x:Reference seedStepper},
                                  Path=Value,
                                  StringFormat='Seed: {0:F0}'}"
                   VerticalOptions="Center" />

            <Stepper x:Name="seedStepper"
                     Minimum="1"
                     ValueChanged="OnStepperValueChanged" />

        </StackLayout>
    </StackLayout>
</ContentPage>

El archivo de código subyacente define una constante para el tamaño del mosaico. El controlador PaintSurface crea un mapa de bits de ese tamaño y un SKCanvas para dibujar en ese mapa de bits. El método SKShader.CreatePerlinNoiseTurbulence crea un sombreador con ese tamaño de mosaico. Este sombreador se dibuja en el mapa de bits:

public partial class TiledPerlinNoisePage : ContentPage
{
    const int TILE_SIZE = 200;

    public TiledPerlinNoisePage()
    {
        InitializeComponent();
    }

    void OnStepperValueChanged(object sender, ValueChangedEventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Get seed value from stepper
        float seed = (float)seedStepper.Value;

        SKRect tileRect = new SKRect(0, 0, TILE_SIZE, TILE_SIZE);

        using (SKBitmap bitmap = new SKBitmap(TILE_SIZE, TILE_SIZE))
        {
            using (SKCanvas bitmapCanvas = new SKCanvas(bitmap))
            {
                bitmapCanvas.Clear();

                // Draw tiled turbulence noise on bitmap
                using (SKPaint paint = new SKPaint())
                {
                    paint.Shader = SKShader.CreatePerlinNoiseTurbulence(
                                        0.02f, 0.02f, 1, seed,
                                        new SKPointI(TILE_SIZE, TILE_SIZE));

                    bitmapCanvas.DrawRect(tileRect, paint);
                }
            }

            // Draw tiled bitmap shader on canvas
            using (SKPaint paint = new SKPaint())
            {
                paint.Shader = SKShader.CreateBitmap(bitmap,
                                                     SKShaderTileMode.Repeat,
                                                     SKShaderTileMode.Repeat);
                canvas.DrawRect(info.Rect, paint);
            }

            // Draw rectangle showing tile
            using (SKPaint paint = new SKPaint())
            {
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Black;
                paint.StrokeWidth = 2;

                canvas.DrawRect(tileRect, paint);
            }
        }
    }
}

Una vez creado el mapa de bits, se usa otro objeto SKPaint para crear un patrón de mapa de bits en mosaico llamando a SKShader.CreateBitmap. Observe los dos argumentos de SKShaderTileMode.Repeat:

paint.Shader = SKShader.CreateBitmap(bitmap,
                                     SKShaderTileMode.Repeat,
                                     SKShaderTileMode.Repeat);

Este sombreador se usa para cubrir el lienzo. Por último, se usa otro objeto SKPaint para trazar un rectángulo que muestra el tamaño del mapa de bits original.

Solo se puede seleccionar el parámetro seed desde la interfaz de usuario. Si se usa el mismo patrón seed en cada plataforma, mostrarán el mismo patrón. Distintos valores de seed dan como resultado patrones diferentes:

Ruido de perlin en mosaico

El patrón cuadrado de 200 píxeles en la esquina superior izquierda fluye sin problemas en los otros mosaicos.

Combinación de varios sombreadores

La clase SKShader incluye un método CreateColor que crea un sombreador con un color sólido especificado. Este sombreador no es muy útil por sí mismo porque simplemente puede establecer ese color en la propiedad Color del objeto SKPaint y establecer la propiedad Shader en un valor nulo.

Este método CreateColor resulta útil en otro método que define SKShader. Este método es CreateCompose, que combina dos sombreadores. Esta es la sintaxis:

public static SKShader CreateCompose (SKShader dstShader, SKShader srcShader);

srcShader (sombreador de origen) se dibuja eficazmente encima de dstShader (sombreador de destino). Si el sombreador de origen es un color sólido o un degradado sin transparencia, el sombreador de destino estará completamente oculto.

Un sombreador de ruido Perlin contiene transparencia. Si ese sombreador es el origen, el sombreador de destino se mostrará a través de las áreas transparentes.

La página de Ruido Perlin compuesto tiene un archivo XAML prácticamente idéntico a la primera página de Ruido Perlin. El archivo de código subyacente también es similar. Pero la página de Ruido Perlin original establece la propiedad Shader de SKPaint en el sombreador devuelto por los métodos CreatePerlinNoiseFractalNoise y CreatePerlinNoiseTurbulence estáticos. Esta página de Ruido Perlin compuesto llama a CreateCompose para una combinación de sombreador. El destino es un sombreador azul sólido creado mediante CreateColor. El origen es un sombreador de ruido Perlin:

public partial class ComposedPerlinNoisePage : ContentPage
{
    public ComposedPerlinNoisePage()
    {
        InitializeComponent();
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnStepperValueChanged(object sender, ValueChangedEventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Get values from sliders and stepper
        float baseFreqX = (float)Math.Pow(10, baseFrequencyXSlider.Value - 4);
        baseFrequencyXText.Text = String.Format("Base Frequency X = {0:F4}", baseFreqX);

        float baseFreqY = (float)Math.Pow(10, baseFrequencyYSlider.Value - 4);
        baseFrequencyYText.Text = String.Format("Base Frequency Y = {0:F4}", baseFreqY);

        int numOctaves = (int)octavesStepper.Value;

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateCompose(
                SKShader.CreateColor(SKColors.Blue),
                SKShader.CreatePerlinNoiseFractalNoise(baseFreqX,
                                                       baseFreqY,
                                                       numOctaves,
                                                       0));

            SKRect rect = new SKRect(0, 0, info.Width, info.Height / 2);
            canvas.DrawRect(rect, paint);

            paint.Shader = SKShader.CreateCompose(
                SKShader.CreateColor(SKColors.Blue),
                SKShader.CreatePerlinNoiseTurbulence(baseFreqX,
                                                     baseFreqY,
                                                     numOctaves,
                                                     0));

            rect = new SKRect(0, info.Height / 2, info.Width, info.Height);
            canvas.DrawRect(rect, paint);
        }
    }
}

El sombreador de ruido fractal está en la parte superior; el sombreador de turbulencia está en la parte inferior:

Ruido de perlin compuesto

Observe el nivel superior de azul de estos sombreadores frente a los mostrados por la página de Ruido Perlin. La diferencia ilustra la cantidad de transparencia en los sombreadores de ruido.

También hay una sobrecarga del método CreateCompose:

public static SKShader CreateCompose (SKShader dstShader, SKShader srcShader, SKBlendMode blendMode);

El parámetro final es un miembro de la enumeración SKBlendMode, una enumeración con 29 miembros que se trata en la siguiente serie de artículos sobre los modos de composición y combinación de SkiaSharp.