Aracılığıyla paylaş


İlişkili Olmayan Dönüşümler

Dönüştürme matrisinin üçüncü sütunuyla perspektif ve kondiri efektleri oluşturma

Çeviri, ölçeklendirme, döndürme ve eğme işlemleri, afin dönüşümleri olarak sınıflandırılır. Affine dönüşümleri paralel çizgileri korur. dönüştürmeden önce iki çizgi paralelse, dönüştürmeden sonra paralel kalırlar. Dikdörtgenler her zaman paralelogramlara dönüştürülür.

Ancak SkiaSharp, dikdörtgeni herhangi bir dışbükey dörtgene dönüştürme özelliğine sahip olan afin olmayan dönüşümlere de sahiptir:

Dışbükey dörtgene dönüştürülmüş bit eşlem

Dışbükey dörtgen, iç açıları her zaman 180 dereceden az olan dört taraflı bir şekildir ve yanları birbiriyle kesişmiyor.

Dönüştürme matrisinin üçüncü satırı 0, 0 ve 1 dışındaki değerlere ayarlandığında, benfin olmayan dönüşümler sonucu verir. Tam SKMatrix çarpma şu şekildedir:

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

Sonuç dönüştürme formülleri şunlardır:

x' = ScaleX·x + EğriltmeX·y + TransX

y' = Eğriltme·x + ScaleY·y + TransY

z' = Persp0·x + Persp1·y + Persp2

İki boyutlu dönüşümler için 3'e 3 matris kullanmanın temel kuralı, her şeyin Z'nin 1'e eşit olduğu düzlemde kalmasıdır. 0 ve Persp1 Persp2 1'e eşit olmadığı sürece Persp0 dönüşüm, Z koordinatlarını bu düzlemin dışına taşımıştır.

Bunu iki boyutlu bir dönüşüme geri yüklemek için koordinatların o düzleme geri taşınması gerekir. Başka bir adım gereklidir. x', y've z' değerleri z'ye bölünmelidir:

x" = x' / z'

y" = y' / z'

z" = z' / z' = 1

Bunlar homojen koordinatlar olarak bilinir ve matematikçi August Ferdinand Möbius tarafından geliştirilmiştir, topolojik tuhaflığıyla çok daha iyi bilinen Möbius Şeridi.

z' 0 ise bölme sonsuz koordinatlarla sonuçlanır. Aslında, Möbius'un homojen koordinatlar geliştirme motivasyonlarından biri sonsuz değerleri sonlu sayılarla temsil etme yeteneğiydi.

Bununla birlikte, grafikleri görüntülerken, sonsuz değerlere dönüşen koordinatlarla bir şeyin işlenmesini önlemek istersiniz. Bu koordinatlar işlenmez. Bu koordinatların çevresindeki her şey çok büyük olacak ve muhtemelen görsel olarak uyumlu olmayacaktır.

Bu denklemde, z' değerinin sıfır olmasını istemezsiniz:

z' = Persp0·x + Persp1·y + Persp2

Sonuç olarak, bu değerlerin bazı pratik kısıtlamaları vardır:

Hücre Persp2 sıfır olabilir veya sıfır olamaz. Sıfır ise Persp2 , nokta için z' sıfırdır (0, 0) ve bu genellikle tercih edilmez çünkü bu nokta iki boyutlu grafiklerde çok yaygındır. Sıfıra eşit değilse Persp2 , 1'de sabitse Persp2 genellik kaybı olmaz. Örneğin, bunun Persp2 5 olması gerektiğini belirlerseniz, matristeki tüm hücreleri 1'e eşit olacak şekilde Persp2 5'e bölebilirsiniz ve sonuç aynı olur.

Bu nedenlerden dolayı, Persp2 genellikle kimlik matrisinde aynı değer olan 1'de sabittir.

Genel olarak Persp0 ve Persp1 küçük sayılardır. Örneğin, bir kimlik matrisiyle başladığınızı ancak 0,01 olarak ayarladığınızı Persp0 varsayalım:

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

Dönüşüm formülleri şunlardır:

x' = x / (0,01·x + 1)

y' = y / (0,01·x + 1)

Şimdi bu dönüşümü kullanarak çıkış noktasında konumlandırılmış 100 piksellik bir kare kutu işlendi. Dört köşe şu şekilde dönüştürülür:

(0, 0) → (0, 0)

(0, 100) → (0, 100)

(100, 0) → (50, 0)

(100, 100) → (50, 50)

x 100 olduğunda, z' paydası 2 olur, bu nedenle x ve y koordinatları etkili bir şekilde yarıya indirilir. Kutunun sağ tarafı sol taraftan kısalır:

Benfin olmayan bir dönüşüme tabi bir kutu

Bu Persp hücre adlarının bölümü "perspektif"e başvurur çünkü foreshortening, kutunun sağ tarafla görüntüleyiciden daha ileriye doğru eğildiğini gösterir.

Test Perspektifi sayfası, ve değerleriyle denemeler yapmanızı ve bunların nasıl çalıştıklarını Persp0 Pers1 hissetmenizi sağlar. Bu matris hücrelerinin makul değerleri o kadar küçüktür kiSlider, Evrensel Windows Platformu bunları düzgün şekilde işleyemez. UWP sorununa uyum sağlamak için TestPerspective.xaml dosyasındaki iki Slider öğe –1 ile 1 arasında olacak şekilde başlatılmalıdır:

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

Arka kod dosyasındaki TestPerspectivePage kaydırıcıların olay işleyicileri, değerleri –0,01 ile 0,01 arasında olacak şekilde 100'e böler. Buna ek olarak, oluşturucu bir bit eşlem içinde yüklenir:

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

İşleyici, PaintSurface bu iki kaydırıcının 100'e bölünen değerlerine göre adlı perspectiveMatrix bir SKMatrix değeri hesaplar. Bu, bu dönüşümün merkezini bit eşlemin ortasına yerleştiren iki çeviri dönüştürmesiyle birleştirilir:

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

Bazı örnek görüntüler şunlardır:

Test Perspektifi sayfasının üçlü ekran görüntüsü

Kaydırıcılarla denemeler yapınca, 0,0066 veya altındaki –0,0066 değerlerinin görüntünün aniden kırılmasına ve tutarsız olmasına neden olduğunu göreceksiniz. Dönüştürülen bit eşlem 300 piksel karedir. Merkezine göre dönüştürülür, dolayısıyla bit eşlem koordinatları –150 ile 150 arasında değişir. z' değerinin şu olduğunu hatırlayın:

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

Veya Persp1 0,0066'dan büyükse veya –0,0066'nın altındaysa Persp0 bit eşleminin her zaman bir koordinatı vardır ve bu da sıfır değerinin z' değerine neden olur. Bu, sıfıra bölmeye neden olur ve işleme bir karışıklığa dönüşür. Benfin olmayan dönüşümleri kullanırken, sıfıra bölmeye neden olan koordinatlarla herhangi bir şeyin işlenmesini önlemek istiyorsunuz.

Genel olarak, ayarlı Persp0 ve Persp1 yalıtımlı olmayacaksınız. Ayrıca, belirli türlerde afin olmayan dönüşümler elde etmek için matristeki diğer hücrelerin ayarlanması da genellikle gereklidir.

Bu tür bir benfin olmayan dönüşüm, bir taper dönüşümüdür. Bu tür bir benfin olmayan dönüşüm dikdörtgenin genel boyutlarını korur, ancak bir tarafı bantlar:

Bir tapa dönüştürmesine tabi bir kutu

sınıfı, TaperTransform bu parametrelere göre afin olmayan bir dönüşümün genelleştirilmiş bir hesaplamasını gerçekleştirir:

  • dönüştürülen görüntünün dikdörtgen boyutu,
  • dikdörtgenin kenarını gösteren bir numaralandırma,
  • nasıl dokunduğunu gösteren başka bir numaralandırma ve
  • bantların kapsamı.

Kod şu şekildedir:

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

Bu sınıf, Taper Dönüşümü sayfasında kullanılır. XAML dosyası, numaralandırma değerlerini seçmek için iki Picker öğe ve konizör kesirini seçmek için bir Slider örneği oluşturur. İşleyici, PaintSurface dönüştürü bit eşleminin sol üst köşesine göre yapmak için iki çeviri dönüşümüyle birleştirir:

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

Burada bazı örnekler verilmiştir:

Kondüktör Dönüştürme sayfasının üç kez ekran görüntüsü

Bir diğer genelleştirilmiş benfin olmayan dönüşüm türü, sonraki makale olan 3B Döndürmeler'de gösterilen 3B döndürmedir.

Afin olmayan dönüşüm, dikdörtgeni herhangi bir dışbükey dörtgene dönüştürebilir. Bu, Bağımsız Olmayan Matrisi Göster sayfası tarafından gösterilir. Matris Dönüşümleri makalesindeki Affine Matrisini Göster sayfasına çok benzer, ancak bit eşlemin dördüncü köşesini işlemek için dördüncü TouchPoint bir nesnesi vardır:

Bağımsız Olmayan Matrisi Göster sayfasının üçlü ekran görüntüsü

Bit eşlem köşelerinden birinin iç açısını 180 dereceden büyük yapmaya çalışmadığınız veya iki kenarın birbiriyle kesişmesini sağlamadığınız sürece, program sınıftan bu yöntemi ShowNonAffineMatrixPage kullanarak dönüşümü başarıyla hesaplar:

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

Hesaplama kolaylığı için, bu yöntem toplam dönüşümü üç ayrı dönüşümün ürünü olarak elde eder ve burada bu dönüşümlerin bit eşlemin dört köşesini nasıl değiştirdiği gösteren oklarla simgelenir:

(0, 0) → (0, 0) → (0, 0) → (x0, y0) (sol üst)

(0, H) → (0, 1) → (0, 1) → (x1, y1) (sol alt)

(W, 0) → (1, 0) → (1, 0) → (x2, y2) (sağ üst)

(W, H) → (1, 1) → (a, b) → (x3, y3) (sağ alt)

Sağdaki son koordinatlar, dört dokunma noktasıyla ilişkili dört noktadır. Bunlar bit eşlem köşelerinin son koordinatlarıdır.

W ve H, bit eşlem genişliğini ve yüksekliğini gösterir. İlk dönüşüm S yalnızca bit eşlemi 1 piksel kareye ölçeklendirir. İkinci dönüşüm, afin olmayan dönüşümdür Nve üçüncüsü de afin dönüşümüdür A. Bu afin dönüşümü üç noktayı temel alır, bu nedenle aynı önceki afin ComputeMatrix yöntemi gibidir ve (a, b) noktasıyla dördüncü satırı içermez.

a ve b değerleri, üçüncü dönüşümün benfin olması için hesaplanır. Kod, afin dönüşümünün tersini alır ve ardından sağ alt köşeyi eşlemek için bunu kullanır. Önemli olan da bu (a, b).

Benfin olmayan dönüşümlerin bir diğer kullanımı da üç boyutlu grafikleri taklit etmektir. Sonraki makalede, 3B Döndürmeler iki boyutlu grafiğin 3B alanda nasıl döndürüldüğünü göreceksiniz.