非仿射转换Non-Affine Transforms

下载示例下载示例Download Sample Download the sample

使用转换矩阵的第三个列创建透视和锥化效果Create perspective and taper effects with the third column of the transform matrix

平移、 缩放、 旋转和倾斜都属于仿射转换。Translation, scaling, rotation, and skewing are all classified as affine transforms. 仿射转换保留平行直线。Affine transforms preserve parallel lines. 如果并行转换之前的两个行,它们并行之后保持转换。If two lines are parallel prior to the transform, they remain parallel after the transform. 矩形始终转换为平行四边形。Rectangles are always transformed to parallelograms.

但是,SkiaSharp 还是非仿射转换,其具有将转换为任何凸四边形的矩形的功能的支持:However, SkiaSharp is also capable of non-affine transforms, which have the capability to transform a rectangle into any convex quadrilateral:

凸四边形是具有内部角度始终小于 180 度和边不会超过每个其他四个边的图形。A convex quadrilateral is a four-sided figure with interior angles always less than 180 degrees and sides that don't cross each other.

非仿射转换矩阵的第三个行设置为 0、 0 和 1 以外的值时转换结果。Non-affine transforms result when the third row of the transform matrix is set to values other than 0, 0, and 1. 完整SKMatrix乘法是:The full SKMatrix multiplication is:

              │ ScaleX  SkewY   Persp0 │
| x  y  1 | × │ SkewX   ScaleY  Persp1 │ = | x'  y'  z' |
              │ TransX  TransY  Persp2 │

结果转换公式是:The resultant transform formulas are:

x = ScaleX·x + SkewX·y + TransXx' = ScaleX·x + SkewX·y + TransX

y = SkewY·x + ScaleY·y + TransYy' = SkewY·x + ScaleY·y + TransY

z = Persp0·x + Persp1·y + Persp2z` = Persp0·x + Persp1·y + Persp2

使用为二维转换的 3 x 3 矩阵的基本原理是,所有内容保留在平面上其中 Z 等于 1。The fundamental rule of using a 3-by-3 matrix for two-dimensional transforms is that everything remains on the plane where Z equals 1. 除非Persp0Persp1均为 0,和Persp2等于 1,则转换已禁用该平面的 Z 坐标。Unless Persp0 and Persp1 are 0, and Persp2 equals 1, the transform has moved the Z coordinates off that plane.

若要还原到一个二维转换这,坐标必须移回到该平面。To restore this to a two-dimensional transform, the coordinates must be moved back to that plane. 另一个步骤是必需的。Another step is required. X,y,和 z 的值必须除以 z:The x', y', and z` values must be divided by z':

x" = x' / z'x" = x' / z'

y"= y / zy" = y' / z'

z"= z / z = 1z" = z' / z' = 1

这些参数称为齐次坐标和 Möbius 条带安年 8 月 Ferdinand Möbius,得更好地已知为他拓扑的奇怪之处,通过其进行开发。These are known as homogeneous coordinates and they were developed by mathematician August Ferdinand Möbius, much better known for his topological oddity, the Möbius Strip.

如果 z 为 0,无限坐标中的部门结果。If z' is 0, the division results in infinite coordinates. 事实上,开发齐次坐标 Möbius 的动机之一是能够表示的无限值的有限数字。In fact, one of Möbius's motivations for developing homogeneous coordinates was the ability to represent infinite values with finite numbers.

在显示时图形,但是,你想要避免呈现内容转换为无限值的坐标。When displaying graphics, however, you want to avoid rendering something with coordinates that transform to infinite values. 不会呈现这些坐标。Those coordinates won't be rendered. 在这些坐标附近出现的所有内容将非常大,可能不以可视方式一致。Everything in the vicinity of those coordinates will be very large and probably not visually coherent.

在此等式,你不希望的 z 值变为 0:In this equation, you do not want the value of z' becoming zero:

z = Persp0·x + Persp1·y + Persp2z` = Persp0·x + Persp1·y + Persp2

因此,这些值存在一些实际的限制:Consequently, these values have some practical restrictions:

Persp2单元格可以是零或不为零。The Persp2 cell can either be zero or not zero. 如果Persp2为零,则 z 为点 (0,0),0,这是通常不可取因为该点是在二维图形中很常见。If Persp2 is zero, then z' is zero for the point (0, 0), and that's usually not desirable because that point is very common in two-dimensional graphics. 如果Persp2不等于零,则不会丢失的一般性如果Persp2固定为 1。If Persp2 is not equal to zero, then there is no loss of generality if Persp2 is fixed at 1. 例如,如果你确定Persp2应为 5,然后您可以只是矩阵中的所有单元格被除 5,这使得Persp2等于 1,并且结果将是相同。For example, if you determine that Persp2 should be 5, then you can simply divide all the cells in the matrix by 5, which makes Persp2 equal to 1, and the result will be the same.

出于这些原因,Persp2通常固定为 1,这是标识矩阵中的相同值。For these reasons, Persp2 is often fixed at 1, which is the same value in the identity matrix.

通常情况下,Persp0Persp1是小的数字。Generally, Persp0 and Persp1 are small numbers. 例如,假设开头恒等矩阵但集Persp0为 0.01:For example, suppose you begin with an identity matrix but set Persp0 to 0.01:

| 1  0   0.01 |
| 0  1    0   |
| 0  0    1   |

转换公式是:The transform formulas are:

x = x / (0.01·x + 1)x` = x / (0.01·x + 1)

y = y / (0.01·x + 1)y' = y / (0.01·x + 1)

现在,使用此转换来呈现原点定位 100 像素方框。Now use this transform to render a 100-pixel square box positioned at the origin. 下面是如何转换的四个角:Here's how the four corners are transformed:

(0,0) → (0,0)(0, 0) → (0, 0)

(0,100) → (0,100)(0, 100) → (0, 100)

(100,0) → (50,0)(100, 0) → (50, 0)

(100,100) → (50,50)(100, 100) → (50, 50)

当 x 为 100,则 z 分母为 2,因此 x 和 y 坐标会有效地缩减了一半。When x is 100, then the z' denominator is 2, so the x and y coordinates are effectively halved. 框的右侧将成为短于左侧和右侧:The right side of the box becomes shorter than the left side:

Persp这些单元格名称的部分是因为透视收缩建议框现在倾斜最荒谬不过在查看器的右侧指"透视"。The Persp part of these cell names refers to "perspective" because the foreshortening suggests that the box is now tilted with the right side further from the viewer.

测试透视页,可以尝试使用的值Persp0Pers1若要了解它们如何工作的。The Test Perspective page allows you to experiment with values of Persp0 and Pers1 to get a feel for how they work. 这些矩阵的单元格的合理值是非常小的Slider在通用 Windows 平台中不能正确处理它们。Reasonable values of these matrix cells are so small that the Slider in the Universal Windows Platform can't properly handle them. 若要容纳 UWP 问题,这两个Slider中的元素 TestPerspective.xaml 需要初始化范围为从 1 到-1:To accommodate the UWP problem, the two Slider elements in the TestPerspective.xaml need to be initialized to range from –1 to 1:

<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.Transforms.TestPerspectivePage"
             Title="Test Perpsective">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Minimum" Value="-1" />
                    <Setter Property="Maximum" Value="1" />
                    <Setter Property="Margin" Value="20, 0" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="persp0Slider"
                Grid.Row="0"
                ValueChanged="OnPersp0SliderValueChanged" />

        <Label x:Name="persp0Label"
               Text="Persp0 = 0.0000"
               Grid.Row="1" />

        <Slider x:Name="persp1Slider"
                Grid.Row="2"
                ValueChanged="OnPersp1SliderValueChanged" />

        <Label x:Name="persp1Label"
               Text="Persp1 = 0.0000"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

事件处理程序中的滑块 TestPerspectivePage 代码隐藏文件中将值划分 100,以便它们 –0.01 和 0.01 之间的范围。The event handlers for the sliders in the TestPerspectivePage code-behind file divide the values by 100 so that they range between –0.01 and 0.01. 此外,在构造函数加载位图中:In addition, the constructor loads in a bitmap:

public partial class TestPerspectivePage : ContentPage
{
    SKBitmap bitmap;

    public TestPerspectivePage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }

    void OnPersp0SliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        Slider slider = (Slider)sender;
        persp0Label.Text = String.Format("Persp0 = {0:F4}", slider.Value / 100);
        canvasView.InvalidateSurface();
    }

    void OnPersp1SliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        Slider slider = (Slider)sender;
        persp1Label.Text = String.Format("Persp1 = {0:F4}", slider.Value / 100);
        canvasView.InvalidateSurface();
    }
    ...
}

PaintSurface处理程序计算SKMatrix名为值perspectiveMatrix除以 100 这些两个滑块的值。The PaintSurface handler calculates an SKMatrix value named perspectiveMatrix based on the values of these two sliders divided by 100. 与结合使用这两个转换将此转换的中心放在中心的位图的转换:This is combined with two translate transforms that put the center of this transform in the center of the bitmap:

public partial class TestPerspectivePage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Calculate perspective matrix
        SKMatrix perspectiveMatrix = SKMatrix.MakeIdentity();
        perspectiveMatrix.Persp0 = (float)persp0Slider.Value / 100;
        perspectiveMatrix.Persp1 = (float)persp1Slider.Value / 100;

        // Center of screen
        float xCenter = info.Width / 2;
        float yCenter = info.Height / 2;

        SKMatrix matrix = SKMatrix.MakeTranslation(-xCenter, -yCenter);
        SKMatrix.PostConcat(ref matrix, perspectiveMatrix);
        SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(xCenter, yCenter));

        // Coordinates to center bitmap on canvas
        float x = xCenter - bitmap.Width / 2;
        float y = yCenter - bitmap.Height / 2;

        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(bitmap, x, y);
    }
}

下面是一些示例图像:Here are some sample images:

在试验滑块时,您会发现值 0.0066 超出或低于 –0.0066 导致要突然变得断开和是不同的图像。As you experiment with the sliders, you'll find that values beyond 0.0066 or below –0.0066 cause the image to suddenly become fractured and incoherent. 所转换的位图是 300 像素正方形。The bitmap being transformed is 300-pixels square. 因此从 –150 为 150 的位图的坐标范围相对于其中心转换。It is transformed relative to its center, so the coordinates of the bitmap range from –150 to 150. 请记住,z 的值是:Recall that the value of z' is:

z = Persp0·x + Persp1·y + 1z` = Persp0·x + Persp1·y + 1

如果Persp0Persp1大于 0.0066 下面 –0.0066,则始终导致 z 位图的一些坐标或值为零。If Persp0 or Persp1 is greater than 0.0066 or below –0.0066, then there is always some coordinate of the bitmap that results in a z' value of zero. 为零,导致部门和呈现变得混乱。That causes division by zero, and the rendering becomes a mess. 在使用非仿射转换,你想要避免呈现任何内容导致被零除的坐标。When using non-affine transforms, you want to avoid rendering anything with coordinates that cause division by zero.

通常情况下,不会设置您Persp0Persp1中隔离。Generally, you won't be setting Persp0 and Persp1 in isolation. 此外通常很有必要,在要实现某些类型的非仿射转换矩阵中设置其他单元格。It's also often necessary to set other cells in the matrix to achieve certain types of non-affine transforms.

是一个此类非仿射转换锥化转换One such non-affine transform is a taper transform. 此类型的非仿射转换将保留整体的矩形的尺寸,但通过逐渐一侧:This type of non-affine transform retains the overall dimensions of a rectangle but tapers one side:

TaperTransform 类执行通用的计算的非仿射转换基于这些参数:The TaperTransform class performs a generalized calculation of a non-affine transform based on these parameters:

  • 所转换图像的矩形的大小the rectangular size of the image being transformed,
  • 一个枚举,指示通过逐渐,矩形的边an enumeration that indicates the side of the rectangle that tapers,
  • 另一个枚举,指示如何通过它逐渐,和another enumeration that indicates how it tapers, and
  • 斜削的范围。the extent of the tapering.

下面是代码:Here's the code:

enum TaperSide { Left, Top, Right, Bottom }

enum TaperCorner { LeftOrTop, RightOrBottom, Both }

static class TaperTransform
{
    public static SKMatrix Make(SKSize size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction)
    {
        SKMatrix matrix = SKMatrix.MakeIdentity();

        switch (taperSide)
        {
            case TaperSide.Left:
                matrix.ScaleX = taperFraction;
                matrix.ScaleY = taperFraction;
                matrix.Persp0 = (taperFraction - 1) / size.Width;

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewY = size.Height * matrix.Persp0;
                        matrix.TransY = size.Height * (1 - taperFraction);
                        break;

                    case TaperCorner.Both:
                        matrix.SkewY = (size.Height / 2) * matrix.Persp0;
                        matrix.TransY = size.Height * (1 - taperFraction) / 2;
                        break;
                }
                break;

            case TaperSide.Top:
                matrix.ScaleX = taperFraction;
                matrix.ScaleY = taperFraction;
                matrix.Persp1 = (taperFraction - 1) / size.Height;

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewX = size.Width * matrix.Persp1;
                        matrix.TransX = size.Width * (1 - taperFraction);
                        break;

                    case TaperCorner.Both:
                        matrix.SkewX = (size.Width / 2) * matrix.Persp1;
                        matrix.TransX = size.Width * (1 - taperFraction) / 2;
                        break;
                }
                break;

            case TaperSide.Right:
                matrix.ScaleX = 1 / taperFraction;
                matrix.Persp0 = (1 - taperFraction) / (size.Width * taperFraction);

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewY = size.Height * matrix.Persp0;
                        break;

                    case TaperCorner.Both:
                        matrix.SkewY = (size.Height / 2) * matrix.Persp0;
                        break;
                }
                break;

            case TaperSide.Bottom:
                matrix.ScaleY = 1 / taperFraction;
                matrix.Persp1 = (1 - taperFraction) / (size.Height * taperFraction);

                switch (taperCorner)
                {
                    case TaperCorner.RightOrBottom:
                        break;

                    case TaperCorner.LeftOrTop:
                        matrix.SkewX = size.Width * matrix.Persp1;
                        break;

                    case TaperCorner.Both:
                        matrix.SkewX = (size.Width / 2) * matrix.Persp1;
                        break;
                }
                break;
        }
        return matrix;
    }
}

在使用此类锥化转换页。This class is used in the Taper Transform page. XAML 文件实例化两个Picker要选择的枚举值,元素和一个Slider选择锥化部分。The XAML file instantiates two Picker elements to select the enumeration values, and a Slider for choosing the taper fraction. PaintSurface 处理程序将使用两个锥化转换将转换结果翻译以便相对于位图左上角的转换:The PaintSurface handler combines the taper transform with two translate transforms to make the transform relative to the upper-left corner of the bitmap:

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

    canvas.Clear();

    TaperSide taperSide = (TaperSide)taperSidePicker.SelectedItem;
    TaperCorner taperCorner = (TaperCorner)taperCornerPicker.SelectedItem;
    float taperFraction = (float)taperFractionSlider.Value;

    SKMatrix taperMatrix =
        TaperTransform.Make(new SKSize(bitmap.Width, bitmap.Height),
                            taperSide, taperCorner, taperFraction);

    // Display the matrix in the lower-right corner
    SKSize matrixSize = matrixDisplay.Measure(taperMatrix);

    matrixDisplay.Paint(canvas, taperMatrix,
        new SKPoint(info.Width - matrixSize.Width,
                    info.Height - matrixSize.Height));

    // Center bitmap on canvas
    float x = (info.Width - bitmap.Width) / 2;
    float y = (info.Height - bitmap.Height) / 2;

    SKMatrix matrix = SKMatrix.MakeTranslation(-x, -y);
    SKMatrix.PostConcat(ref matrix, taperMatrix);
    SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(x, y));

    canvas.SetMatrix(matrix);
    canvas.DrawBitmap(bitmap, x, y);
}

下面是一些可能的恶意活动:Here are some examples:

另一种通用非仿射转换是在下一篇文章中所示的三维旋转 3D 旋转Another type of generalized non-affine transforms is 3D rotation, which is demonstrated in the next article, 3D Rotations.

非仿射转换可以将矩形转换为任何凸四边形。The non-affine transform can transform a rectangle into any convex quadrilateral. 这可通过演示显示非仿射矩阵页。This is demonstrated by the Show Non-Affine Matrix page. 它是非常类似于显示仿射矩阵页上,从矩阵转换一文,只不过它具有第四个TouchPoint对象操作中的第四角的位图:It is very similar to the Show Affine Matrix page from the Matrix Transforms article except that it has a fourth TouchPoint object to manipulate the fourth corner of the bitmap:

只要未尝试使内部角度的四个角位图大于 180 度或进行相互交叉的两个方面,该程序已成功计算从使用此方法的转换 ShowNonAffineMatrixPage 类:As long as you don't attempt to make an interior angle of one of the corners of the bitmap greater than 180 degrees, or make two sides cross each other, the program successfully calculates the transform using this method from the ShowNonAffineMatrixPage class:

static SKMatrix ComputeMatrix(SKSize size, SKPoint ptUL, SKPoint ptUR, SKPoint ptLL, SKPoint ptLR)
{
    // Scale transform
    SKMatrix S = SKMatrix.MakeScale(1 / size.Width, 1 / size.Height);

    // Affine transform
    SKMatrix A = new SKMatrix
    {
        ScaleX = ptUR.X - ptUL.X,
        SkewY = ptUR.Y - ptUL.Y,
        SkewX = ptLL.X - ptUL.X,
        ScaleY = ptLL.Y - ptUL.Y,
        TransX = ptUL.X,
        TransY = ptUL.Y,
        Persp2 = 1
    };

    // Non-Affine transform
    SKMatrix inverseA;
    A.TryInvert(out inverseA);
    SKPoint abPoint = inverseA.MapPoint(ptLR);
    float a = abPoint.X;
    float b = abPoint.Y;

    float scaleX = a / (a + b - 1);
    float scaleY = b / (a + b - 1);

    SKMatrix N = new SKMatrix
    {
        ScaleX = scaleX,
        ScaleY = scaleY,
        Persp0 = scaleX - 1,
        Persp1 = scaleY - 1,
        Persp2 = 1
    };

    // Multiply S * N * A
    SKMatrix result = SKMatrix.MakeIdentity();
    SKMatrix.PostConcat(ref result, S);
    SKMatrix.PostConcat(ref result, N);
    SKMatrix.PostConcat(ref result, A);

    return result;
}

为了便于计算,此方法获取作为三个单独的转换,且显示这些转换如何修改位图的四个角箭头此处表示产品的总转换:For ease of calculation, this method obtains the total transform as a product of three separate transforms, which are symbolized here with arrows showing how these transforms modify the four corners of the bitmap:

(0,0) (0,0) 的 → → (0,0) → (x,0,y0) (左上角)(0, 0) → (0, 0) → (0, 0) → (x0, y0) (upper-left)

(0,H) → (0,1) (0,1) → → (x1,y1) (左下方)(0, H) → (0, 1) → (0, 1) → (x1, y1) (lower-left)

(W,0) → (1,0) (1,0) 的 → → (x2,y2) (右上方)(W, 0) → (1, 0) → (1, 0) → (x2, y2) (upper-right)

(W,H) → (1,1) (a,b) → → (x3 y3) (右下方)(W, H) → (1, 1) → (a, b) → (x3, y3) (lower-right)

在右侧的最后一个坐标是与四个触摸点相关联的四个点。The final coordinates at the right are the four points associated with the four touch points. 这些是位图的边角的最后一个坐标。These are the final coordinates of the corners of the bitmap.

W 和 H 表示宽度和位图的高度。W and H represent the width and height of the bitmap. 第一个转换S只是缩放为 1 像素正方形的位图。The first transform S simply scales the bitmap to a 1-pixel square. 第二个转换为非仿射转换N,和第三个是仿射转换AThe second transform is the non-affine transform N, and the third is the affine transform A. 该仿射转换基于三个点,因此它只需像前面仿射 ComputeMatrix 方法并不涉及具有的第四个行 (a,b) 点。That affine transform is based on three points, so it's just like the earlier affine ComputeMatrix method and doesn't involve the fourth row with the (a, b) point.

ab值进行计算,以便第三个转换为仿射转换。The a and b values are calculated so that the third transform is affine. 该代码获取仿射变换的逆变换,然后使用该映射的右下角。The code obtains the inverse of the affine transform and then uses that to map the lower-right corner. 这是点 (a,b)。That's the point (a, b).

非仿射转换的另一个用途是模拟三维图形。Another use of non-affine transforms is to mimic three-dimensional graphics. 在下一篇文章中, 3D 旋转您了解如何将二维图形在 3D 空间中的。In the next article, 3D Rotations you see how to rotate a two-dimensional graphic in 3D space.