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
針對對 α 角度的呼叫 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 的縮放比例來完成。
所有這些轉換類型都會分類為 仿 射轉換。 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
、、、、、SkewY
、TransY
Persp1
Persp0
ScaleY
和 Persp2
來設定或取得九個值。 SkewX
TransX
、 Persp0
和儲存格會在非 Affine Transforms 一文中討論。Persp2
Persp1
如果這些儲存格的預設值為 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
值:
MakeTranslation
MakeScale
MakeScale
具有樞紐點MakeRotation
以弧度為單位的角度MakeRotation
表示具有樞紐點之弧度的角度MakeRotationDegrees
MakeRotationDegrees
具有樞紐點MakeSkew
SKMatrix
也會定義數個靜態方法,這些方法會串連兩個矩陣,這表示將它們相乘。 這些方法會命名為 Concat
、 PostConcat
和 PreConcat
,而且每個都有兩個版本。 這些方法沒有傳回值;相反地,它們會透過ref
自變數參考現有的SKMatrix
值。 在下列範例中, A
、 B
和 R
(針對 “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
構函式搭配靜態 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);
這些方法不會將旋轉轉換串連至現有的轉換。 方法會設定矩陣的所有儲存格。 它們的功能與 MakeRotation
和 MakeRotationDegrees
方法相同,不同之處在於它們不會具現化 SKMatrix
值。
假設您有 SKPath
想要顯示的物件,但您偏好其方向稍有不同,或有不同的中心點。 您可以使用 自變數呼叫 Transform
的方法SKPath
SKMatrix
,以修改該路徑的所有座標。 [ 路徑轉換 ] 頁面示範如何執行這項操作。 類別 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
再次呼叫 ,則會再次套用轉換,而您可以返回的唯一方式是套用另一個復原轉換的矩陣。 幸運的是,結構 SKMatrix
會 TryInvert
定義方法,以取得反轉指定矩陣的矩陣:
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
會顯示可拖曳在螢幕上的半透明圓形。 TouchPoint
SKCanvasView
需要 具有附加之 父系的 SKCanvasView
TouchEffect
或 專案。 將 Capture
屬性設為 true
。 在事件處理程式中TouchAction
,程式必須針對每個TouchPoint
實例呼叫 ProcessTouchEvent
中的 TouchPoint
方法。 如果觸控事件導致觸控點移動,此方法會傳回 true
。 此外, PaintSurface
處理程式必須呼叫 Paint
每個 TouchPoint
實例中的方法,並傳遞至 SKCanvas
物件。
TouchPoint
示範 SkiaSharp 視覺效果可以封裝在個別類別中的常見方式。 類別可以定義屬性來指定視覺效果的特性,而具有SKCanvas
自變數的方法Paint
可以轉譯它。
的 Center
TouchPoint
屬性表示 物件的位置。 這個屬性可以設定為初始化位置;當用戶在畫布周圍拖曳圓形時,屬性就會變更。
[ 顯示 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
的縮放轉換會將位圖縮放為一圖元方形矩形。 複合矩陣×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
根據該矩陣轉譯位圖、在畫面底部顯示矩陣,並在位圖的三個角落呈現觸控點:
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 畫面會在頁面第一次載入時顯示點陣圖,而其他兩個畫面則會在進行一些操作之後顯示:
雖然觸控點似乎拖曳點圖的角落,但這隻是一種錯覺。 從觸控點計算的矩陣會轉換位圖,讓角落與觸控點一致。
使用者更自然地移動、重設大小和旋轉點陣圖,而不是藉由拖曳角落,而是直接在物件上使用一或兩根手指來拖曳、捏合和旋轉。 下一篇文章 說明觸控操作。
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 的二維平面上,但 SkewXZ
和 SkewYZ
儲存格實際上會變成二維轉譯因數。
這就是三維線性轉換作為二維非線性轉換的方式。 (比方說,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 一文。