Share via


SkiaSharp での行列変換

"汎用的な変換行列を使った SkiaSharp の変換について詳しく説明します"

SKCanvas オブジェクトに適用されるすべての変換は、SKMatrix 構造の単一インスタンスに統合されます。 これは、最新のすべての 2 次元グラフィックス システムと同様に、標準の 3 × 3 の変換行列です。

これまで見てきたように、変換行列について知らなくても SkiaSharp で変換を使用できます。ただし、変換行列は理論的な観点から重要であり、変換を使ってパスを変更する場合や、複雑なタッチ入力を処理する場合に非常に重要です。この 2 つについては、この記事と次の記事で説明します。

アフィン変換の対象となるビットマップ

SKCanvas に適用されている現在の変換行列は、読み取り専用の TotalMatrix プロパティにアクセスすることでいつでも使用できます。 SetMatrix メソッドを使って新しい変換行列を設定できます。また、ResetMatrix を呼び出してその変換行列を既定値に戻すことができます。

キャンバスの行列変換を直接操作する他の SKCanvas メンバーは、2 つの行列を乗算して連結する Concat のみです。

既定の変換行列は単位行列であり、対角線上のセルにある 1 と他のすべての場所にある 0 で構成されます。

| 1  0  0 |
| 0  1  0 |
| 0  0  1 |

静的 SKMatrix.MakeIdentity メソッドを使って単位行列を作成できます。

SKMatrix matrix = SKMatrix.MakeIdentity();

SKMatrix の既定のコンストラクターは単位行列を返し "ません"。 すべてのセルが 0 に設定された行列が返されます。 これらのセルを手動で設定する予定がある場合を除き、SKMatrix コンストラクターを使わないでください。

SkiaSharp がグラフィカル オブジェクトをレンダリングすると、各点 (x, y) は、実質的に 3 列目に 1 を含む 1 × 3 の行列に変換されます。

| x  y  1 |

この 1 × 3 の行列は、Z 座標が 1 に設定された 3 次元の点を表します。 2 次元行列変換を 3 次元で行う必要があることには、数学的な理由があります (後で説明します)。 この 1 × 3 の行列は、3 次元座標系の点を表しますが、常に Z が 1 である 2 次元平面上にあるものと考えることができます。

次に、この 1 × 3 の行列に変換行列が乗算され、その結果がキャンバス上にレンダリングされた点になります。

              | 1  0  0 |
| x  y  1 | × | 0  1  0 | = | x'  y'  z' |
              | 0  0  1 |

標準の行列乗算を使うと、変換された点は次のようになります。

x' = x

y' = y

z' = 1

それが既定の変換です。

SKCanvas オブジェクトに対して Translate メソッドを呼び出すと、Translate メソッドの引数 txty は、変換行列の 3 行目の最初の 2 つのセルになります。

|  1   0   0 |
|  0   1   0 |
| tx  ty   1 |

乗算は次のようになります。

              |  1   0   0 |
| x  y  1 | × |  0   1   0 | = | x'  y'  z' |
              | tx  ty   1 |

変換の数式を次に示します。

x' = x + tx

y' = y + ty

拡大縮小係数の既定値は 1 です。 新しい SKCanvas オブジェクトに対して Scale メソッドを呼び出すと、結果として得られる変換行列には、対角線上のセルに引数 sxsy が含まれます。

              | sx   0   0 |
| x  y  1 | × |  0  sy   0 | = | x'  y'  z' |
              |  0   0   1 |

変換の数式は次のとおりです。

x' = sx · x

y' = sy · y

Skew を呼び出した後の変換行列には、拡大縮小係数に隣接する行列セルに 2 つの引数が含まれています。

              │   1   ySkew   0 │
| x  y  1 | × │ xSkew   1     0 │ = | x'  y'  z' |
              │   0     0     1 │

変換の数式は次のとおりです。

x' = x + xSkew · y

y' = ySkew · x + y

角度 α に対する RotateDegrees または RotateRadians の呼び出しの場合、変換行列は次のようになります。

              │  cos(α)  sin(α)  0 │
| x  y  1 | × │ –sin(α)  cos(α)  0 │ = | x'  y'  z' |
              │    0       0     1 │

変換の数式を次に示します。

x' = cos(α) · x - sin(α) · y

y' = sin(α) · x - cos(α) · y

α が 0 度の場合は単位行列です。 α が 180 度の場合、変換行列は次のようになります。

| –1   0   0 |
|  0  –1   0 |
|  0   0   1 |

180 度の回転は、オブジェクトを水平および垂直に反転することと同じです。拡大縮小係数を -1 に設定してこれを実現することもできます。

このような種類の変換はすべて "アフィン" 変換に分類されます。 アフィン変換に行列の 3 列目が関与することはなく、既定値の 0、0、1 のままです。 「非アフィン変換」の記事では、非アフィン変換について説明しています。

行列乗算

変換行列を使うことの大きな利点の 1 つは、行列の乗算によって複合変換を得られることです。これは、SkiaSharp ドキュメントでは "連結" と呼ばれることがよくあります。 SKCanvas の変換関連メソッドの多くは "事前連結" ("pre-concat") を参照しています。これは乗算の順序を参照しますが、行列の乗算は可換ではないため、重要です。

たとえば、Translate メソッドのドキュメントには "移動を指定して現在の行列を事前連結する" と記載されていますが、Scale メソッドのドキュメントには "拡大縮小を指定して現在の行列を事前連結する" と記載されています。

これは、メソッド呼び出しで指定された変換が乗数 (左側のオペランド) であり、現在の変換行列が被乗数 (右側のオペランド) であることを意味します。

Translate の後に Scale が呼び出されたとします。

canvas.Translate(tx, ty);
canvas.Scale(sx, sy);

Scale 変換には、複合変換行列の Translate 変換が乗算されます。

| sx   0   0 |   |  1   0   0 |   | sx   0   0 |
|  0  sy   0 | × |  0   1   0 | = |  0  sy   0 |
|  0   0   1 |   | tx  ty   1 |   | tx  ty   1 |

次のように、ScaleTranslate の前に呼び出すことができます。

canvas.Scale(sx, sy);
canvas.Translate(tx, ty);

その場合、乗算の順序が逆になり、実質的に拡大縮小係数が移動係数に適用されます。

|  1   0   0 |   | sx   0   0 |   |  sx      0    0 |
|  0   1   0 | × |  0  sy   0 | = |   0     sy    0 |
| tx  ty   1 |   |  0   0   1 |   | tx·sx  ty·sy  1 |

ピボット ポイントがある Scale メソッドを次に示します。

canvas.Scale(sx, sy, px, py);

これは、次の移動と拡大縮小の呼び出しと同じです。

canvas.Translate(px, py);
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);

3 つの変換行列は、メソッドがコードで出現する順序とは逆の順序で乗算されます。

|  1    0   0 |   | sx   0   0 |   |  1   0  0 |   |    sx         0     0 |
|  0    1   0 | × |  0  sy   0 | × |  0   1  0 | = |     0        sy     0 |
| –px  –py  1 |   |  0   0   1 |   | px  py  1 |   | px–px·sx  py–py·sy  1 |

SKMatrix 構造体

SKMatrix 構造体を使って、変換行列の 9 つのセルに対応する型 float の 9 つの読み取りまたは書き込みのプロパティを定義します。

│ ScaleX  SkewY   Persp0 │
│ SkewX   ScaleY  Persp1 │
│ TransX  TransY  Persp2 │

また、SKMatrix を使って、型 float[]Values というプロパティを定義します。 このプロパティを使うと、ScaleXSkewXTransXSkewYScaleYTransYPersp0Persp1Persp2 の順に 9 つの値を一度に設定または取得できます。

Persp0Persp1Persp2 セルについては、「非アフィン変換」の記事で説明されています。 これらのセルの既定値が 0、0、1 である場合、変換には次のように座標点が乗算されます。

              │ ScaleX  SkewY   0 │
| x  y  1 | × │ SkewX   ScaleY  0 │ = | x'  y'  z' |
              │ TransX  TransY  1 │

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

y' = SkewX · x + ScaleY · y + TransY

z' = 1

これは完全な 2 次元アフィン変換です。 アフィン変換では平行線が保持されます。つまり、四角形が平行四辺形以外に変換されることはありません。

SKMatrix 構造体を使って、SKMatrix 値を作成する静的メソッドを定義します。 これらはすべて SKMatrix 値を返します。

また、SKMatrix を使って、2 つの行列を連結する、つまり乗算するいくつかの静的メソッドも定義します。 これらのメソッドには ConcatPostConcatPreConcat という名前が付けられており、それぞれ 2 つのバージョンがあります。 これらのメソッドには戻り値がありません。代わりに、ref 引数を通じて既存の SKMatrix 値を参照します。 次の例では、("result" の) ABR はすべて SKMatrix 値です。

2 つの Concat メソッドは次のように呼び出されます。

SKMatrix.Concat(ref R, A, B);

SKMatrix.Concat(ref R, ref A, ref B);

次の乗算が実行されます。

R = B × A

他のメソッドには 2 つのパラメーターしかありません。 最初のパラメーターが変更され、メソッド呼び出しから戻ったら、2 つの行列の積が含まれます。 2 つの PostConcat メソッドは次のように呼び出されます。

SKMatrix.PostConcat(ref A, B);

SKMatrix.PostConcat(ref A, ref B);

これらの呼び出しは次の演算を実行します。

A = A × B

2 つの PreConcat メソッドは似ています。

SKMatrix.PreConcat(ref A, B);

SKMatrix.PreConcat(ref A, ref B);

これらの呼び出しは次の演算を実行します。

A = B × A

このようにすべての引数が ref であるメソッドのバージョンは、基になる実装を呼び出す際に若干効率的ですが、コードを読む側にとっては、引数が ref であるものはすべてこのメソッドによって変更されると想定するため、混乱を招く可能性があります。 さらに、多くの場合、Make メソッドのいずれかの結果である引数を渡すと便利です。次に例を示します。

SKMatrix result;
SKMatrix.Concat(result, SKMatrix.MakeTranslation(100, 100),
                        SKMatrix.MakeScale(3, 3));

これにより、次の行列が作成されます。

│   3    0  0 │
│   0    3  0 │
│ 100  100  1 │

これは、拡大縮小変換に移動変換を乗算したものです。 この特定のケースでは、SKMatrix 構造体は SetScaleTranslate というメソッドを含むショートカットを提供します。

SKMatrix R = new SKMatrix();
R.SetScaleTranslate(3, 3, 100, 100);

これは、SKMatrix コンストラクターを安全に使用できる数少ない状況の 1 つです。 SetScaleTranslate メソッドを使って、行列の 9 つのセルすべてを設定します。 SKMatrix コンストラクターを静的な Rotate および RotateDegrees メソッドと共に使っても安全です。

SKMatrix R = new SKMatrix();

SKMatrix.Rotate(ref R, radians);

SKMatrix.Rotate(ref R, radians, px, py);

SKMatrix.RotateDegrees(ref R, degrees);

SKMatrix.RotateDegrees(ref R, degrees, px, py);

これらのメソッドを使っても、回転変換は既存の変換に連結 "されません"。 これらのメソッドを使って、行列のすべてのセルを設定します。 これらは、SKMatrix 値のインスタンスを作成しないことを除けば、機能的には MakeRotation および MakeRotationDegrees メソッドと同じです。

表示したい SKPath オブジェクトがあっても、その向きや中心点は多少異なる方が望ましい場合があるとします。 SKMatrix 引数を指定して SKPathTransform メソッドを呼び出すことで、そのパスのすべての座標を変更できます。 [パス変換] ページには、これを行う方法が示されます。 PathTransform クラスはフィールド内の HendecagramPath オブジェクトを参照しますが、そのパスに変換を適用するには、そのコンストラクターを使います。

public class PathTransformPage : ContentPage
{
    SKPath transformedPath = HendecagramArrayPage.HendecagramPath;

    public PathTransformPage()
    {
        Title = "Path Transform";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        SKMatrix matrix = SKMatrix.MakeScale(3, 3);
        SKMatrix.PostConcat(ref matrix, SKMatrix.MakeRotationDegrees(360f / 22));
        SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(300, 300));

        transformedPath.Transform(matrix);
    }
    ...
}

HendecagramPath オブジェクトの中心は (0, 0) にあり、星の 11 個の点はその中心から全方向に 100 単位ずつ広がっています。 これは、パスに正と負の両方の座標があることを意味します。 [パス変換] ページでは、3 倍の大きさの星とすべて正の座標を使うことが選択されます。 さらに、星の 1 つの点が真上を向くことは避けられています。 そうではなく、星の 1 つの点が真下を向くようにしています (星の点は 11 個なので、両方がそうなることはありません)。これには、360 度を 22 で除算した分、星を回転させる必要があります。

コンストラクターは、次のパターンで PostConcat メソッドを使い、3 つの個別の変換から SKMatrix オブジェクトを構築します (この A、B、C は SKMatrix のインスタンスです)。

SKMatrix matrix = A;
SKMatrix.PostConcat(ref A, B);
SKMatrix.PostConcat(ref A, C);

これは連続する一連の乗算であるため、結果は次のようになります。

A × B × C

連続する乗算は、各変換の実行内容を理解するのに役立ちます。 拡大縮小変換によってパス座標のサイズは 3 倍に大きくなるため、座標の範囲は - 300 から 300 になります。 回転変換により、原点を中心にして星が回転されます。 次に、移動変換によって右と下に 300 ピクセルずつシフトされるため、すべての座標が正になります。

同じ行列を生成するシーケンスは他にもあります。 もう 1 つは次のとおりです。

SKMatrix matrix = SKMatrix.MakeRotationDegrees(360f / 22);
SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(100, 100));
SKMatrix.PostConcat(ref matrix, SKMatrix.MakeScale(3, 3));

これにより、まず中心点を中心にしてパスが回転され、次に右と下に 100 ピクセル移動され、すべての座標が正になります。 次に、星のサイズは、新しい左上隅 (点 (0, 0)) に対して大きくなります。

PaintSurface ハンドラーを使うと、このパスを単純にレンダリングできます。

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

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Magenta;
            paint.StrokeWidth = 5;

            canvas.DrawPath(transformedPath, paint);
        }
    }
}

キャンバスの左上隅に表示されます。

[パス変換] ページのトリプル スクリーンショット

このプログラムのコンストラクターにより、次の呼び出しで行列がパスに適用されます。

transformedPath.Transform(matrix);

パスはこの行列をプロパティとして保持 "しません"。 代わりに、パスのすべての座標に変換が適用されます。 Transform が再度呼び出された場合は、変換が再度適用されます。元に戻す唯一の方法は、変換を元に戻す別の行列を適用することです。 幸いなことに、SKMatrix 構造体は、指定された行列を反転する行列を取得する TryInvert メソッドを定義しています。

SKMatrix inverse;
bool success = matrix.TryInverse(out inverse);

このメソッドは TryInverse と呼ばれます。すべての行列が可逆とは言えませんが、非可逆行列がグラフィックス変換に使われる可能性は低いためです。

プログラム内の SKPoint 値、点の配列、SKRect、または単なる 1 つの数値に行列変換を適用することもできます。 SKMatrix 構造体は、次のように単語 Map で始まるメソッドのコレクションを使ったこれらの演算をサポートしています。

SKPoint transformedPoint = matrix.MapPoint(point);

SKPoint transformedPoint = matrix.MapPoint(x, y);

SKPoint[] transformedPoints = matrix.MapPoints(pointArray);

float transformedValue = matrix.MapRadius(floatValue);

SKRect transformedRect = matrix.MapRect(rect);

この最後のメソッドを使う場合は、SKRect 構造体では回転された四角形を表現できないことに注意してください。 移動と拡大縮小を表す SKMatrix 値に対してのみ、このメソッドが役に立ちます。

対話型実験

アフィン変換の感覚を身に付ける 1 つの方法は、画面上でビットマップの 3 つの角を対話形式で動かし、どのような変換結果が得られるかを確認することです。 これが [アフィン行列の表示] ページの背後にある考え方です。 このページには、他のデモでも使われる 2 つのクラスがさらに必要です。

TouchPoint クラスを使って、画面上でドラッグできる半透明の円を表示します。 TouchPoint を使うには、SKCanvasView、または SKCanvasView の親である要素に TouchEffect がアタッチされている必要があります。 Capture プロパティを trueに設定します。 TouchAction イベント ハンドラーでは、プログラムにより、TouchPoint で各 TouchPoint インスタンスに対して ProcessTouchEvent メソッドを呼び出す必要があります。 タッチ イベントによってタッチ ポイントが移動した場合、このメソッドは true を返します。 また、PaintSurface ハンドラーを使って各 TouchPoint インスタンスで Paint メソッドを呼び出し、それに SKCanvas オブジェクトを渡す必要があります。

TouchPoint は、SkiaSharp ビジュアルを別のクラスにカプセル化する一般的な方法を示しています。 このクラスを使うと、ビジュアルの特性を指定するためのプロパティを定義できます。また、SKCanvas 引数がある Paint というメソッドを使ってそれをレンダリングできます。

TouchPointCenter プロパティは、オブジェクトの位置を示します。 場所を初期化するようにこのプロパティを設定することができます。ユーザーがキャンバス上で円をドラッグすると、プロパティは変化します。

[アフィン行列ページの表示] にも MatrixDisplay クラスが必要です。 このクラスを使って、SKMatrix オブジェクトのセルを表示します。 これには 2 つのパブリック メソッドがあります。レンダリングされた行列の次元を取得する Measure と、それを表示する Paint です。 このクラスには、別のフォント サイズまたは色に置き換えることができる型 SKPaintMatrixPaint プロパティが含まれています。

ShowAffineMatrixPage.xaml ファイルを使って SKCanvasView のインスタンスを作成し、TouchEffect をアタッチします。 ShowAffineMatrixPage.xaml.cs 分離コード ファイルを使って 3 つの TouchPoint オブジェクトを作成し、埋め込みリソースから読み込むビットマップの 3 つの角に対応する位置にそれらを設定します。

public partial class ShowAffineMatrixPage : ContentPage
{
    SKMatrix matrix;
    SKBitmap bitmap;
    SKSize bitmapSize;

    TouchPoint[] touchPoints = new TouchPoint[3];

    MatrixDisplay matrixDisplay = new MatrixDisplay();

    public ShowAffineMatrixPage()
    {
        InitializeComponent();

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

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

        touchPoints[0] = new TouchPoint(100, 100);                  // upper-left corner
        touchPoints[1] = new TouchPoint(bitmap.Width + 100, 100);   // upper-right corner
        touchPoints[2] = new TouchPoint(100, bitmap.Height + 100);  // lower-left corner

        bitmapSize = new SKSize(bitmap.Width, bitmap.Height);
        matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center,
                                           touchPoints[1].Center,
                                           touchPoints[2].Center);
    }
    ...
}

アフィン行列は 3 つの点によって一意に定義されます。 3 つの TouchPoint オブジェクトは、ビットマップの左上、右上、左下の角に対応します。 アフィン行列は四角形を平行四辺形に変換することしかできないため、4 つ目の点は他の 3 つの点によって暗黙的に示されます。 コンストラクターは最後に ComputeMatrix を呼び出します。これにより、これら 3 つの点から SKMatrix オブジェクトのセルが計算されます。

TouchAction ハンドラーを使って各 TouchPointProcessTouchEvent メソッドを呼び出します。 scale 値は、Xamarin.Forms 座標からピクセルに変換されます。

public partial class ShowAffineMatrixPage : ContentPage
{
    ...
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        bool touchPointMoved = false;

        foreach (TouchPoint touchPoint in touchPoints)
        {
            float scale = canvasView.CanvasSize.Width / (float)canvasView.Width;
            SKPoint point = new SKPoint(scale * (float)args.Location.X,
                                        scale * (float)args.Location.Y);
            touchPointMoved |= touchPoint.ProcessTouchEvent(args.Id, args.Type, point);
        }

        if (touchPointMoved)
        {
            matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center,
                                               touchPoints[1].Center,
                                               touchPoints[2].Center);
            canvasView.InvalidateSurface();
        }
    }
    ...
}

いずれかの TouchPoint が移動した場合、メソッドによって再度 ComputeMatrix が呼び出され、画面は無効になります。

ComputeMatrix メソッドを使って、これら 3 つの点が暗黙的に示す行列を決定します。 A という行列により、3 つの点に基づいて 1 ピクセルの正方形が平行四辺形に変換されます。一方、S という拡大縮小変換により、ビットマップは 1 ピクセルの正方形に拡大縮小されます。 複合行列は S × A です。

public partial class ShowAffineMatrixPage : ContentPage
{
    ...
    static SKMatrix ComputeMatrix(SKSize size, SKPoint ptUL, SKPoint ptUR, SKPoint ptLL)
    {
        // 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
        };

        SKMatrix result = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref result, A, S);
        return result;
    }
    ...
}

最後に、PaintSurface メソッドを使ってその行列に基づいてビットマップをレンダリングし、その行列を画面の下部に表示し、ビットマップの 3 つの角にタッチ ポイントをレンダリングします。

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

        canvas.Clear();

        // Display the bitmap using the matrix
        canvas.Save();
        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(bitmap, 0, 0);
        canvas.Restore();

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

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

        // Display the touchpoints
        foreach (TouchPoint touchPoint in touchPoints)
        {
            touchPoint.Paint(canvas);
        }
    }
  }

次の iOS 画面は、ページが最初に読み込まれたときのビットマップを示しています。一方、他の 2 つの画面は、何らかの操作をした後のビットマップを示しています。

[アフィン行列の表示] ページのトリプル スクリーンショット

タッチ ポイントがビットマップの角をドラッグしているように見えますが、それは単なる錯覚です。 タッチ ポイントから計算された行列により、角がタッチ ポイントと一致するようにビットマップが変換されます。

ユーザーにとって、ビットマップの移動、サイズ変更、回転を行うには、角をドラッグするのではなく、オブジェクト上で 1 本または 2 本の指を直接使ってドラッグ、ピンチ、回転する方が自然です。 この点については、次の記事「タッチ操作」で説明します。

3 × 3 行列の理由

2 次元グラフィックス システムに必要なのは 2 × 2 の変換行列のみだと思われるかもしれません。

           │ ScaleX  SkewY  │
| x  y | × │                │ = | x'  y' |
           │ SkewX   ScaleY │

これは、拡大縮小、回転、さらには傾斜には機能しますが、最も基本的な変換である移動はできません。

問題は、2 × 2 の行列が 2 次元の "線形" 変換を表すことです。 線形変換ではいくつかの基本的な算術演算は保存されますが、その意味することの 1 つは、線形変換では点 (0, 0) が決して変更されないということです。 線形変換では移動が不可能になります。

3 次元では、線形変換行列は次のようになります。

              │ ScaleX  SkewYX  SkewZX │
| x  y  z | × │ SkewXY  ScaleY  SkewZY │ = | x'  y'  z' |
              │ SkewXZ  SkewYZ  ScaleZ │

SkewXY というラベルが付いたセルは、Y の値に基づいて値が X 座標を傾斜させることを意味します。セル SkewXZ は、Z の値に基づいて値が X 座標を傾斜させることを意味します。他の Skew セルでも同様に値が傾斜させます。

SkewZXSkewZY を 0 に、ScaleZ を 1 に設定することで、この 3 次元変換行列を 2 次元平面に制限することができます。

              │ ScaleX  SkewYX   0 │
| x  y  z | × │ SkewXY  ScaleY   0 │ = | x'  y'  z' |
              │ SkewXZ  SkewYZ   1 │

Z が 1 である 3 次元空間の平面上に 2 次元グラフィックスが完全に描画される場合、変換乗算は次のようになります。

              │ ScaleX  SkewYX   0 │
| x  y  1 | × │ SkewXY  ScaleY   0 │ = | x'  y'  1 |
              │ SkewXZ  SkewYZ   1 │

Z が 1 である 2 次元平面上にすべてが留まりますが、SkewXZSkewYZ の各セルは事実上 2 次元の移動係数になります。

このようにして、3 次元の線形変換が 2 次元の非線形変換として機能します (同様に、3 次元グラフィックスの変換は 4 × 4 の行列に基づいています)。

SkiaSharp の SKMatrix 構造体を使って、その 3 行目のプロパティを定義します。

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

Persp0Persp1 が 0 以外の値である場合、Z が 1 である 2 次元平面からオブジェクトを移動する変換になります。 これらのオブジェクトをその平面に戻すとどうなるかについては、「非アフィン変換」に関する記事で説明されています。