비아핀(Non-Affine) 변환
변환 행렬의 세 번째 열을 사용하여 원근 및 테이퍼 효과 만들기
변환, 크기 조정, 회전 및 기울이기 모두 아핀 변환으로 분류됩니다. 아핀 변환은 병렬 선을 유지합니다. 두 줄이 변환 전에 병렬이면 변환 후 병렬로 기본. 사각형은 항상 병렬로 변환됩니다.
그러나 SkiaSharp는 직사각형을 모든 사각형 사분면으로 변환할 수 있는 비아핀 변환도 가능합니다.
공록 사분면은 내부 각도가 항상 180도 미만이고 서로 교차하지 않는 측면이 있는 4면 그림입니다.
비아핀 변환은 변환 행렬의 세 번째 행이 0, 0 및 1 이외의 값으로 설정된 경우 발생합니다. 전체 SKMatrix
곱셈은 다음과 같습니다.
│ ScaleX SkewY Persp0 │ | x y 1 | × │ SkewX ScaleY Persp1 │ = | x' y' z' | │ TransX TransY Persp2 │
결과 변환 수식은 다음과 같습니다.
x' = ScaleX·x + SkewX·y + TransX
y' = SkewY·x + ScaleY·y + TransY
z' = Persp0·x + Persp1·y + Persp2
2차원 변환에 3 x 3 행렬을 사용하는 기본 규칙은 Z가 1인 평면에서 모든 것이 다시 기본 것입니다. 0이고 Persp1
1이 Persp2
아니면 Persp0
변환이 해당 평면에서 Z 좌표를 이동했습니다.
이를 2차원 변환으로 복원하려면 좌표를 해당 평면으로 다시 이동해야 합니다. 또 다른 단계가 필요합니다. x', y', z' 값을 z'로 나누어야 합니다.
x" = x' / z'
y" = y' / z'
z" = z' / z' = 1
이들은 동질 좌표로 알려져 있으며, 그들은 훨씬 더 그의 토폴로지 이상한, 뫼비우스 스트립으로 알려진 수학자 아우구스트 페르디난드 뫼비우스에 의해 개발되었다.
z'가 0이면 나누기에서 무한 좌표가 생성됩니다. 사실, 뫼비우스의 동질 좌표 개발 동기 중 하나는 유한 숫자로 무한 값을 나타내는 능력이었습니다.
그러나 그래픽을 표시할 때는 무한 값으로 변환되는 좌표로 렌더링하지 않으려고 합니다. 이러한 좌표는 렌더링되지 않습니다. 이러한 좌표 부근의 모든 항목은 매우 크고 시각적으로 일관되지 않을 수 있습니다.
이 수식에서는 z'의 값이 0이 되는 것을 원하지 않습니다.
z' = Persp0·x + Persp1·y + Persp2
따라서 이러한 값에는 몇 가지 실질적인 제한이 있습니다.
셀은 Persp2
0이거나 0이 아닐 수 있습니다. 0이면 Persp2
z'가 점(0, 0)에 대해 0이고 2차원 그래픽에서 해당 점이 매우 일반적이기 때문에 일반적으로 바람직하지 않습니다. 0과 같지 않으면 Persp2
1에서 고정된 경우 Persp2
일반성이 손실되지 않습니다. 예를 들어 5로 결정 Persp2
하면 행렬의 모든 셀을 5 Persp2
로 나누면 1과 같아지고 결과는 동일합니다.
이러한 이유로 Persp2
ID 행렬에서 동일한 값인 1에서 고정되는 경우가 많습니다.
일반적으로 작은 Persp0
Persp1
숫자입니다. 예를 들어 ID 행렬로 시작하지만 0.01로 설정 Persp0
한다고 가정합니다.
| 1 0 0.01 | | 0 1 0 | | 0 0 1 |
변환 수식은 다음과 같습니다.
x' = x / (0.01·x + 1)
y' = y / (0.01·x + 1)
이제 이 변환을 사용하여 원점 위치에 배치된 100픽셀 정사각형 상자를 렌더링합니다. 네 개의 모서리가 변환되는 방법은 다음과 같습니다.
(0, 0) →(0, 0)
(0, 100) →(0, 100)
(100, 0) →(50, 0)
(100, 100) →(50, 50)
x가 100이면 z의 분모는 2이므로 x와 y 좌표는 효과적으로 절반으로 줄입니다. 상자의 오른쪽이 왼쪽보다 짧아집니다.
Persp
이러한 셀 이름의 부분은 "큐브 뷰어"를 참조하는데, 이는 예를 들어 이제 상자가 뷰어에서 오른쪽으로 기울어져 있음을 나타내기 때문입니다.
테스트 큐브 뷰 페이지에서 값을 Persp0
실험하고 Pers1
작동 방식에 대한 느낌을 얻을 수 있습니다. 이러한 행렬 셀의 적절한 값이 너무 작아 Slider
서 유니버설 Windows 플랫폼 해당 셀을 제대로 처리할 수 없습니다. UWP 문제를 수용하려면 TestPerspective.xaml의 두 Slider
요소를 –1에서 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
대한 이벤트 처리기는 -0.01에서 0.01 사이의 범위가 되도록 값을 100으로 나눕니다. 또한 생성자는 비트맵에 로드됩니다.
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
처리기는 이러한 두 슬라이더의 값을 100으로 나눈 값에 따라 명명된 perspectiveMatrix
값을 계산 SKMatrix
합니다. 이 변환은 비트맵의 가운데에 이 변환의 중심을 두는 두 개의 변환 변환과 결합됩니다.
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);
}
}
다음은 몇 가지 샘플 이미지입니다.
슬라이더를 실험할 때 0.0066 또는 –0.0066 미만의 값으로 인해 이미지가 갑자기 골절되고 일관되지 않습니다. 변환되는 비트맵은 300픽셀 정사각형입니다. 중심을 기준으로 변환되므로 비트맵의 좌표 범위는 –150에서 150까지입니다. z'의 값은 다음과 같습니다.
z' = Persp0·x + Persp1·y + 1
0.0066보다 크거나 Persp1
–0.0066보다 작은 경우 Persp0
항상 비트맵의 일부 좌표로 인해 z 값이 0이 됩니다. 이로 인해 나누기(0)가 발생하고 렌더링이 엉망이 됩니다. 비아핀 변환을 사용하는 경우 0으로 나누기를 유발하는 좌표로 아무것도 렌더링하지 않으려고 합니다.
일반적으로 설정 Persp0
Persp1
되지 않으며 격리됩니다. 또한 특정 유형의 비아핀 변환을 달성하기 위해 행렬의 다른 셀을 설정해야 하는 경우가 많습니다.
이러한 비아핀 변환 중 하나는 테이퍼 변환입니다. 이 유형의 비아핀 변환은 사각형의 전체 차원을 유지하지만 한쪽은 테이퍼합니다.
클래스는 TaperTransform
다음 매개 변수를 기반으로 비아핀 변환의 일반화된 계산을 수행합니다.
- 변환되는 이미지의 사각형 크기입니다.
- 테이퍼가 되는 사각형의 측면을 나타내는 열거형입니다.
- 테이퍼 방법을 나타내는 다른 열거형 및
- 테이퍼링의 범위입니다.
코드는 다음과 같습니다.
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;
}
}
이 클래스는 테이퍼 변환 페이지에서 사용됩니다. XAML 파일은 두 Picker
요소를 인스턴스화하여 열거형 값을 Slider
선택하고 테이퍼 분수를 선택합니다. PaintSurface
처리기는 테이퍼 변환을 두 개의 변환 변환과 결합하여 비트맵의 왼쪽 위 모서리를 기준으로 변환을 만듭니다.
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);
}
다음 몇 가지 예를 참조하세요.
일반화된 비아핀 변환의 또 다른 유형은 다음 문서 인 3D 회전에 설명된 3D 회전입니다.
비아핀 변환은 사각형을 모든 공자 사분면으로 변환할 수 있습니다. 이는 비아핀 행렬 표시 페이지에서 설명합니다. 비트맵의 네 번째 모서리를 조작하는 네 번째 TouchPoint
개체가 있다는 점을 제외하면 행렬 변환 문서의 아핀 행렬 표시 페이지와 매우 유사합니다.
비트맵의 모서리 중 하나의 내부 각도를 180도 이상 만들거나 양면이 서로 교차하도록 시도하지 않는 한 프로그램은 클래스에서 ShowNonAffineMatrixPage
이 메서드를 사용하여 변환을 성공적으로 계산합니다.
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;
}
계산 편의를 위해 이 메서드는 총 변환을 세 개의 개별 변환의 곱으로 가져오며, 여기서는 이러한 변환이 비트맵의 네 모서리를 수정하는 방법을 보여 주는 화살표로 기호화됩니다.
(0, 0) →(0, 0) →(0, 0) →(x0, y0)(왼쪽 위)
(0, H) →(0, 1) →(0, 1) →(x1, y1)(왼쪽 아래)
(W, 0) →(1, 0) →(1, 0) →(x2, y2)(오른쪽 위)
(W, H) →(1, 1) →(a, b) →(x3, y3)(오른쪽 아래)
오른쪽의 마지막 좌표는 4개의 터치 포인트와 연결된 4개의 점입니다. 비트맵 모서리의 마지막 좌표입니다.
W와 H는 비트맵의 너비와 높이를 나타냅니다. 첫 번째 변환 S
은 비트맵의 크기를 1픽셀 정사각형으로 조정합니다. 두 번째 변환은 비아핀 변환이고, 세 번째 변환 N
은 아핀 변환 A
입니다. 해당 아핀 변환은 3점을 기반으로 하므로 이전의 아핀 ComputeMatrix
메서드와 같으며 (a, b) 점과 네 번째 행을 포함하지 않습니다.
a
세 번째 변환이 아핀이 되도록 값 및 b
값이 계산됩니다. 코드는 아핀 변환의 역방향을 가져온 다음, 이를 사용하여 오른쪽 아래 모서리를 매핑합니다. 이것이 포인트(a, b)입니다.
비아핀 변환의 또 다른 사용은 3차원 그래픽을 모방하는 것입니다. 다음 문서에서 는 3D 회전 을 통해 3D 공간에서 2차원 그래픽을 회전하는 방법을 확인할 수 있습니다.