SkiaSharp 噪点与组合

简单的矢量图形往往看起来不自然。 直线、平滑曲线和纯色与现实世界物体的缺陷并不相似。 在为 1982 年电影《Tron》制作计算机生成的图形时,计算机科学家 Ken Perlin 开始开发算法,使用随机过程为这些图像提供更真实的纹理。 1997 年,Ken Perlin 荣获奥斯卡技术成就奖。 他的成就称为 Perlin 噪点,并在 SkiaSharp 中得到支持。 下面是一个示例:

柏林噪声示例

你可以看到,每个像素都不是随机的颜色值。 像素到像素的连续性导致随机形状。

Skia 中对 Perlin 噪点的支持基于 CSS 和 SVG 的 W3C 规范。 滤镜效果模块级别 1 第 8.20 节包含 C 代码中的基础 Perlin 噪点算法。

探索 Perlin 噪点

SKShader 类定义了两种不同的静态方法来生成 Perlin 噪点:CreatePerlinNoiseFractalNoiseCreatePerlinNoiseTurbulence。 参数相同:

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);

这个种方法也存在于带有附加 SKPointI 参数的重载版本中。 平铺 Perlin 噪点部分讨论了这些重载。

两个 baseFrequency 参数是 SkiaSharp 文档中定义的正值,范围为 0 到 1,但也可以将其设置为更高的值。 值越高,随机图像在水平和垂直方向上的变化越大。

numOctaves 值是 1 或更大的整数。 它与算法中的迭代因子有关。 每个额外的八度音阶会产生前一个八度音阶一半的效果,因此效果会随着八度音阶值的增加而减弱。

seed 参数是随机数生成器的起点。 尽管指定为浮点值,但小数在使用之前会被截断,并且 0 与 1 相同。

示例中的“柏林噪声”页面允许你试验 baseFrequencynumOctaves 参数的各种值。 这是 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>

它使用两个 Slider 视图来表示两个 baseFrequency 参数。 为了扩大较低值的范围,滑块是对数形式的。 代码隐藏文件根据 Slider 值的幂计算 SKShader 方法的参数。 Label 视图显示计算值:

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);

Slider 值 1 对应于 0.001,Slider 值 2 对应于 0.01,Slider 值 3 对应于 0.1,Slider 值 4 对应于 1。

这是包含该代码的代码隐藏文件:

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);
        }
    }
}

下面是在 iOS、Android 和通用 Windows 平台 (UWP) 设备上运行的程序。 分形噪点显示在画布的上半部分。 湍流噪点位于下半部分:

柏林噪声

相同的参数始终产生从左上角开始的相同模式。 当你调整 UWP 窗口的宽度和高度时,这种一致性显而易见。 当 Windows 10 重新绘制屏幕时,画布上半部分的图案保持不变。

噪点图案包含不同程度的透明度。 如果在 canvas.Clear() 调用中设置颜色,透明度就会变得明显。 该颜色在图案中变得突出。 你还会在组合多个着色器部分看到此效果。

这些 Perlin 噪点模式很少单独使用。 它们通常会受到后续文章中讨论的混合模式和滤色器的影响。

平铺 Perlin 噪点

用于创建 Perlin 噪点的两个静态 SKShader 方法也存在于重载版本中。 CreatePerlinNoiseFractalNoiseCreatePerlinNoiseTurbulence 重载有一个附加的 SKPointI 参数:

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);

SKPointI 结构是熟悉的 SKPoint 结构的整数版本。 SKPointI 定义类型为 int 而不是 floatXY 属性。

这些方法创建指定大小的重复图案。 在每个图块中,右边缘与左边缘相同,顶部边缘与底部边缘相同。 “平铺 Perlin 噪点”页演示了此特征。 XAML 文件与以上示例类似,但它只有一个用于更改 seed 参数的 Stepper 视图:

<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>

代码隐藏文件定义了图块大小的常量。 PaintSurface 处理程序创建该大小的位图,以及要绘制到该位图中的 SKCanvasSKShader.CreatePerlinNoiseTurbulence 方法创建具有该图块大小的着色器。 此着色器绘制在位图上:

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);
            }
        }
    }
}

创建位图后,另一个 SKPaint 对象用于通过调用 SKShader.CreateBitmap 创建平铺位图模式。 请注意 SKShaderTileMode.Repeat 的两个参数:

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

此着色器用于覆盖画布。 最后,另一个 SKPaint 对象用于绘制显示原始位图大小的矩形。

只能从用户界面中选择 seed 参数。 如果每个平台上使用相同的 seed 图案,它们将显示相同的图案。 不同的 seed 值会导致不同的图案:

平铺柏林噪声

左上角的 200 像素正方形图案无缝流入其他图块中。

组合多个着色器

SKShader 类包含一个 CreateColor 方法,用于创建具有指定纯色的着色器。 此着色器本身并不是很有用,因为你只需将该颜色设置为 SKPaint 对象的 Color 属性,并将 Shader 属性设置为 null。

CreateColor 方法在 SKShader 定义的另一个方法中十分有用。 此方法为 CreateCompose,它组合了两个着色器。 语法如下:

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

srcShader(源着色器)实际上绘制在 dstShader(目标着色器)之上。 如果源着色器是纯色或不透明的渐变,则目标着色器将被完全遮挡。

Perlin 噪点着色器包含透明度。 如果该着色器是源,则目标着色器将通过透明区域显示。

“组合的 Perlin 噪点”页包含一个 XAML 文件,该文件与第一个“Perlin 噪点”页几乎相同。 代码隐藏文件也类似。 但是,原始“Perlin 噪点”页将 SKPaintShader 属性设置为从静态 CreatePerlinNoiseFractalNoiseCreatePerlinNoiseTurbulence 方法返回的着色器。 此“组合的 Perlin 噪点”页调用 CreateCompose 以实现组合着色器。 目标是使用 CreateColor 创建的纯蓝色着色器。 源是 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);
        }
    }
}

分形噪点着色器位于顶部;湍流着色器位于底部:

组合柏林噪声

请注意这些着色器比“Perlin 噪点”页显示的着色器要蓝多少。 这种差异说明了噪点着色器中的透明程度。

CreateCompose 方法也有一个重载:

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

最后一个参数是 SKBlendMode 枚举的成员,该枚举有 29 个成员,我们将在下一个有关 SkiaSharp 合成与混合模式的系列文章中讨论该枚举。