Share via


SkiaSharp 中的矩陣轉換

使用多用途轉換矩陣深入探討SkiaSharp轉換

套用至 SKCanvas 物件的所有轉換都會合併在 結構的單一實例中 SKMatrix 。 這是標準 3 by-3 轉換矩陣,類似於所有新式 2D 圖形系統中的轉換矩陣。

如您所見,您可以在SkiaSharp中使用轉換而不瞭解轉換矩陣,但轉換矩陣在理論上很重要,在使用轉換來修改路徑或處理複雜觸控輸入時非常重要,本文和下一篇文章都會示範這兩者。

受仿射轉換的點陣圖

套用至 SKCanvas 的目前轉換矩陣隨時都可以存取唯讀 TotalMatrix 屬性。 您可以使用 方法來設定新的轉換矩陣 SetMatrix ,而且您可以藉由呼叫 ResetMatrix將矩陣還原為預設值。

唯一直接使用畫布矩陣轉換的另一個 SKCanvas 成員,就是 Concat 將兩個矩陣串連在一起。

預設轉換矩陣是識別矩陣,由1在對角線儲存格和 0 的其他地方組成:

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

您可以使用靜態 SKMatrix.MakeIdentity 方法來建立識別矩陣:

SKMatrix matrix = SKMatrix.MakeIdentity();

默認建SKMatrix構函式不會傳回識別矩陣。 它會傳回所有儲存格設定為零的矩陣。 除非您打算手動設定這些儲存格,否則請勿使用 建 SKMatrix 構函式。

當 SkiaSharp 轉譯圖形物件時,每個點 (x, y) 會有效地轉換成第三欄中有 1 個的 1 個矩陣:

| x  y  1 |

這個 1 by-3 矩陣代表三維點,Z 座標設定為 1。 有數學原因(稍後討論)為什麼二維矩陣轉換需要在三個維度中運作。 您可以將這個 1 by-3 矩陣視為在 3D 座標系統中代表一個點,但一律位於 Z 等於 1 的 2D 平面上。

接著,這個 1-by-3 矩陣會乘以轉換矩陣,而結果是在畫布上轉譯的點:

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

使用標準矩陣乘法,轉換的點如下所示:

x' = x

y' = y

z' = 1

這是預設轉換。

Translate在物件上SKCanvas呼叫 方法時,tx方法的 和 ty 自變數Translate會成為轉換矩陣第三列中的前兩個單元格:

|  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 方法時,結果轉換矩陣會在sx對角線儲存格中包含和 sy 自變數:

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

轉換公式如下所示:

x' = sx · x

y' = sy · y

呼叫 Skew 后的轉換矩陣包含與縮放因數相鄰之矩陣單元格中的兩個自變數:

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

轉換公式如下:

x' = x + xSkew · y

y' = ySkew · x + y

針對對 α 角度的呼叫 RotateDegreesRotateRadians ,轉換矩陣如下所示:

              │  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 的縮放比例來完成。

所有這些轉換類型都會分類為 仿 射轉換。 Affine 轉換永遠不會牽涉到矩陣的第三個數據行,其會維持在預設值 0、0 和 1。 非 Affine 轉換一文討論非仿射轉換。

矩陣乘法

使用轉換矩陣的其中一個顯著優點是,復合轉換可以透過矩陣乘法取得,這通常會在SkiaSharp檔中 稱為串連。 中的 SKCanvas 許多轉換相關方法都是指「預先串連」或「預先 concat」。這是指乘法的順序,這很重要,因為矩陣乘法不是通勤的。

例如,方法的檔 Translate 指出,它會「使用指定的轉譯預先 concat 目前的矩陣」,而方法的檔 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 |

Scale 可以呼叫之前 Translate ,如下所示:

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

三個轉換矩陣會以反向順序乘以程式代碼中方法的方式:

|  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 會定義對應至轉換矩陣九個儲存格之類型的 float 九個讀取/寫入屬性:

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

SKMatrix也會定義名為類型的float[]屬性Values。 這個屬性可用來以一次次順序ScaleX、、、、、SkewYTransYPersp1Persp0ScaleYPersp2來設定或取得九個值。 SkewXTransX

Persp0和儲存格會在非 Affine Transforms 一文中討論。Persp2Persp1 如果這些儲存格的預設值為 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

這是完整的二維仿射轉換。 仿射轉換會保留平行線條,這表示矩形永遠不會轉換成平行投影以外的任何專案。

結構 SKMatrix 會定義數個靜態方法來建立 SKMatrix 值。 所有這些傳回 SKMatrix 值:

SKMatrix 也會定義數個靜態方法,這些方法會串連兩個矩陣,這表示將它們相乘。 這些方法會命名為 ConcatPostConcatPreConcat,而且每個都有兩個版本。 這些方法沒有傳回值;相反地,它們會透過ref自變數參考現有的SKMatrix值。 在下列範例中, ABR (針對 “result”) 都是 SKMatrix 值。

這兩 Concat 種方法會像這樣呼叫:

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

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

這些會執行下列乘法:

R = B × A

其他方法只有兩個參數。 第一個參數已修改,而且從方法呼叫傳回時,會包含兩個矩陣的乘積。 這兩 PostConcat 種方法會像這樣呼叫:

SKMatrix.PostConcat(ref A, B);

SKMatrix.PostConcat(ref A, ref B);

這些呼叫會執行下列作業:

A = A × B

這兩 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 建構函式的次數之一。 方法會 SetScaleTranslate 設定矩陣的所有九個儲存格。 使用建 SKMatrix 構函式搭配靜態 RotateRotateDegrees 方法也是安全的:

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

這些方法不會將旋轉轉換串連至現有的轉換。 方法會設定矩陣的所有儲存格。 它們的功能與 MakeRotationMakeRotationDegrees 方法相同,不同之處在於它們不會具現化 SKMatrix 值。

假設您有 SKPath 想要顯示的物件,但您偏好其方向稍有不同,或有不同的中心點。 您可以使用 自變數呼叫 Transform 的方法SKPathSKMatrix,以修改該路徑的所有座標。 [ 路徑轉換 ] 頁面示範如何執行這項操作。 類別 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 個單位。 這表示路徑同時具有正座標和負座標。 [ 路徑轉換 ] 頁面偏好使用三倍大的星號,以及所有正座標。 此外,它不希望一個點的恆星直指。 它寧願讓一點星直指。 (因為明星有11分,所以不能有兩分。這需要將恆星旋轉 360 度除以 22。

建構函式會使用 具有下列模式的 方法,從PostConcat三個不同的轉換建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 像素,因此所有座標都會變成正數。

還有其他序列會產生相同的矩陣。 以下是另一個:

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 再次呼叫 ,則會再次套用轉換,而您可以返回的唯一方式是套用另一個復原轉換的矩陣。 幸運的是,結構 SKMatrixTryInvert 定義方法,以取得反轉指定矩陣的矩陣:

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

因為並非所有矩陣都是可反轉的,但無法反轉的矩陣可能無法用於圖形轉換,因此會呼叫 TryInverse 方法。

您也可以將矩陣轉換套用至 SKPoint 值、點陣列、 SKRect甚至程式內的單一數位。 結構 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 代表翻譯和調整的值。

互動式實驗

取得仿射轉換感覺的其中一種方式,是在畫面上以互動方式移動位圖的三個角落,並查看轉換結果。 這是 [顯示 Affine 矩陣] 頁面背後的概念。 此頁面需要其他兩個類別,這些類別也用於其他示範:

類別 TouchPoint 會顯示可拖曳在螢幕上的半透明圓形。 TouchPointSKCanvasView需要 具有附加之 父系的 SKCanvasViewTouchEffect 或 專案。 將 Capture 屬性設為 true。 在事件處理程式中TouchAction,程式必須針對每個TouchPoint實例呼叫 ProcessTouchEvent 中的 TouchPoint 方法。 如果觸控事件導致觸控點移動,此方法會傳回 true 。 此外, PaintSurface 處理程式必須呼叫 Paint 每個 TouchPoint 實例中的方法,並傳遞至 SKCanvas 物件。

TouchPoint 示範 SkiaSharp 視覺效果可以封裝在個別類別中的常見方式。 類別可以定義屬性來指定視覺效果的特性,而具有SKCanvas自變數的方法Paint可以轉譯它。

CenterTouchPoint 屬性表示 物件的位置。 這個屬性可以設定為初始化位置;當用戶在畫布周圍拖曳圓形時,屬性就會變更。

[ 顯示 Affine 矩陣頁面 ] 也需要 類別 MatrixDisplay 。 這個類別會顯示 物件的儲存格 SKMatrix 。 它有兩個公用方法: Measure 取得轉譯矩陣的維度,並 Paint 加以顯示。 類別包含 MatrixPaint 類型的 SKPaint 屬性,可以針對不同的字型大小或色彩來取代。

ShowAffineMatrixPage.xaml 檔案會具現化 SKCanvasView 並附加 TouchEffect ShowAffineMatrixPage.xaml.cs程式代碼後置檔案會建立三TouchPoint個物件,然後將它們設定為對應至它從內嵌資源載入之位圖三角的位置:

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

仿射矩陣是由三個點唯一定義的。 這三 TouchPoint 個對象會對應至位圖的左上角、右上角和左下角。 由於相依矩陣只能夠將矩形轉換成平行方圖,因此其他三個點會隱含第四個點。 建構函式會以呼叫 ComputeMatrix結束,它會從這三個 SKMatrix 點計算物件的單元格。

處理程式 TouchAction 會呼叫 ProcessTouchEvent 每個 TouchPoint的方法。 值 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 會決定這三個點所隱含的矩陣。 名為 A 的矩陣會根據三點將一圖元方形矩形轉換成平行投影,而名為 S 的縮放轉換會將位圖縮放為一圖元方形矩形。 複合矩陣×SA

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 根據該矩陣轉譯位圖、在畫面底部顯示矩陣,並在位圖的三個角落呈現觸控點:

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 畫面會在頁面第一次載入時顯示點陣圖,而其他兩個畫面則會在進行一些操作之後顯示:

[顯示 Affine 矩陣] 頁面的三個螢幕快照

雖然觸控點似乎拖曳點圖的角落,但這隻是一種錯覺。 從觸控點計算的矩陣會轉換位圖,讓角落與觸控點一致。

使用者更自然地移動、重設大小和旋轉點陣圖,而不是藉由拖曳角落,而是直接在物件上使用一或兩根手指來拖曳、捏合和旋轉。 下一篇文章 說明觸控操作

3 by-3 矩陣的原因

預期二維圖形系統只需要 2 位元組 2 轉換矩陣:

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

這適用於縮放、旋轉甚至扭曲,但它無法進行最基本的轉換,也就是翻譯。

問題是,2 對 2 矩陣代表兩個 維度中的線性 轉換。 線性轉換會保留一些基本的算術運算,但其中一個含意是線性轉換永遠不會改變點 (0, 0)。 線性轉換會使翻譯變得不可能。

在三個維度中,線性轉換矩陣看起來像這樣:

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

標示 SkewXY 的儲存格表示值會根據 Y 的值扭曲 X 座標;數據格 SkewXZ 表示值會根據 Z 的值扭曲 X 座標;而值則與其他 Skew 儲存格類似扭曲。

藉由將 和 SkewZY 設定SkewZX為 0,並將這個 3D 轉換矩陣限制為二維平面,並將ScaleZ此 3D 轉換矩陣限制為 1:

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

如果平面上的二維圖形完全繪製在 Z 等於 1 的平面上,轉換乘法看起來像這樣:

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

所有專案都停留在 Z 等於 1 的二維平面上,但 SkewXZSkewYZ 儲存格實際上會變成二維轉譯因數。

這就是三維線性轉換作為二維非線性轉換的方式。 (比方說,3D 圖形中的轉換是以 4 by-4 矩陣為基礎。

SKMatrix SkiaSharp 中的 結構會定義該第三個資料列的屬性:

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

的非零值 Persp0 ,並 Persp1 導致將物件從 Z 等於 1 的二維平面移動的轉換。 當這些物件移回該平面時會發生什麼事,請參閱非 Affine Transforms 一文