Udostępnij za pośrednictwem


Rotacje 3D w SkiaSharp

Użyj przekształceń innych niż affine, aby obrócić obiekty 2D w przestrzeni 3D.

Jednym z typowych zastosowań przekształceń innych niż affine jest symulowanie obrotu obiektu 2D w przestrzeni 3D:

Ciąg tekstowy obracany w przestrzeni 3D

To zadanie obejmuje pracę z rotacjami trójwymiarowymi, a następnie wyprowadzanie transformacji nieaplikowanej SKMatrix , która wykonuje te rotacje 3D.

Trudno jest opracować tę SKMatrix transformację działającą wyłącznie w dwóch wymiarach. Zadanie staje się znacznie łatwiejsze, gdy ta macierz 3-by-3 pochodzi z macierzy 4-by-4 używanej w grafice 3D. SkiaSharp obejmuje klasę SKMatrix44 do tego celu, ale niektóre tło w grafice 3D jest niezbędne do zrozumienia rotacji 3D i macierzy transformacji 4-po 4.

Trójwymiarowy układ współrzędnych dodaje trzecią oś o nazwie Z. Koncepcyjnie oś Z znajduje się pod kątem prostym na ekranie. Punkty współrzędnych w przestrzeni 3D są wskazywane trzema liczbami: (x, y, z). W systemie współrzędnych 3D używanym w tym artykule zwiększenie wartości X jest na prawo i zwiększenie wartości Y spada, podobnie jak w dwóch wymiarach. Zwiększenie dodatnich wartości Z wychodzi z ekranu. Początek to lewy górny róg, podobnie jak w grafice 2D. Ekran można traktować jako płaszczyznę XY z osią Z pod kątami prostymi do tej płaszczyzny.

Jest to nazywane układem współrzędnych po lewej stronie. Jeśli wskażesz forefinger lewej strony w kierunku dodatnich współrzędnych X (z prawej strony), a środkowy palec w kierunku zwiększenia współrzędnych Y (w dół), to punkty kciuka w kierunku zwiększania współrzędnych Z — rozciągając się od ekranu.

W grafice 3D przekształcenia są oparte na macierzy 4-by-4. Oto macierz tożsamości 4-by-4:

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

W pracy z macierzą 4-by-4 wygodnie jest zidentyfikować komórki z ich numerami wierszy i kolumn:

|  M11  M12  M13  M14  |
|  M21  M22  M23  M24  |
|  M31  M32  M33  M34  |
|  M41  M42  M43  M44  |

Jednak klasa SkiaSharp Matrix44 jest nieco inna. Jedynym sposobem ustawiania lub pobierania poszczególnych wartości komórek jest SKMatrix44 użycie indeksatora Item . Indeksy wierszy i kolumn są oparte na zera, a nie na podstawie jednej, a wiersze i kolumny są zamieniane. Komórka M14 na powyższym diagramie jest dostępna przy użyciu indeksatora [3, 0]SKMatrix44 w obiekcie.

W systemie graficznym 3D punkt 3D (x, y, z) jest konwertowany na macierz 1-by-4 do mnożenia przez macierz przekształcania 4-by-4:

                 |  M11  M12  M13  M14  |
| x  y  z  1 | × |  M21  M22  M23  M24  | = | x'  y'  z'  w' |
                 |  M31  M32  M33  M34  |
                 |  M41  M42  M43  M44  |

Analogicznie do przekształceń 2D, które mają miejsce w trzech wymiarach, przyjmuje się, że przekształcenia 3D mają mieć miejsce w czterech wymiarach. Czwarty wymiar jest określany jako W, a przestrzeń 3D zakłada się, że istnieje w przestrzeni 4D, gdzie współrzędne W są równe 1. Formuły przekształcania są następujące:

x' = M11·x + M21·y + M31·z + M41

y' = M12·x + M22·y + M32·z + M42

z' = M13·x + M23·y + M33·z + M43

w' = M14·x + M24·y + M34·z + M44

Z formuł transformacji wynika, że komórki M11, M33M22, są czynnikami skalowania w kierunkach M42M41X, Y i Z oraz , i M43 są czynnikami tłumaczenia w kierunkach X, Y i Z.

Aby przekonwertować te współrzędne z powrotem na przestrzeń 3D, gdzie W równa 1, współrzędne x', y i z są dzielone przez w':

x" = x' / w'

y" = y' / w'

z" = z' / w'

w" = w' / w' = 1

Ten podział według w' zapewnia perspektywę w przestrzeni 3D. Jeśli wartość w jest równa 1, perspektywa nie występuje.

Rotacje w przestrzeni 3D mogą być dość złożone, ale najprostsze rotacje to te wokół osi X, Y i Z. Rotacja kąta α wokół osi X jest następująca macierz:

|  1     0       0     0  |
|  0   cos(α)  sin(α)  0  |
|  0  –sin(α)  cos(α)  0  |
|  0     0       0     1  |

Wartości X pozostają takie same w przypadku poddania tej transformacji. Obrót wokół osi Y pozostawia wartości Y bez zmian:

|  cos(α)  0  –sin(α)  0  |
|    0     1     0     0  |
|  sin(α)  0   cos(α)  0  |
|    0     0     0     1  |

Obrót wokół osi Z jest taki sam jak w grafice 2D:

|  cos(α)  sin(α)  0  0  |
| –sin(α)  cos(α)  0  0  |
|    0       0     1  0  |
|    0       0     0  1  |

Kierunek obrotu jest implikowany przez przekazanie układu współrzędnych. Jest to system leworęczny, więc jeśli wskażesz kciuk lewej ręki w kierunku rosnących wartości dla określonej osi — po prawej stronie dla obrotu wokół osi X, w dół w celu obrotu wokół osi Y, a w kierunku obrotu wokół osi Z — krzywa innych palców wskazuje kierunek obrotu dla kątów dodatnich.

SKMatrix44 ma uogólnione metody statyczne CreateRotation i CreateRotationDegrees , które umożliwiają określenie osi, wokół której odbywa się obrót:

public static SKMatrix44 CreateRotationDegrees (Single x, Single y, Single z, Single degrees)

Dla obrotu wokół osi X ustaw pierwsze trzy argumenty na 1, 0, 0. Dla obrotu wokół osi Y ustaw je na 0, 1, 0 i dla obrotu wokół osi Z, ustaw je na 0, 0, 1.

Czwarta kolumna 4-by-4 jest dla perspektywy. Nie SKMatrix44 ma metod tworzenia przekształceń perspektyw, ale można utworzyć je samodzielnie przy użyciu następującego kodu:

SKMatrix44 perspectiveMatrix = SKMatrix44.CreateIdentity();
perspectiveMatrix[3, 2] = -1 / depth;

Przyczyna nazwy depth argumentu będzie widoczna wkrótce. Ten kod tworzy macierz:

|  1  0  0      0     |
|  0  1  0      0     |
|  0  0  1  -1/depth  |
|  0  0  0      1     |

Formuły przekształcania powodują następujące obliczenie elementu w':

w' = –z / depth + 1

Służy to do zmniejszenia współrzędnych X i Y, gdy wartości Z są mniejsze niż zero (koncepcyjnie za płaszczyzną XY) i zwiększyć współrzędne X i Y dla wartości dodatnich Z. Gdy współrzędna Z równa depth, wartość w' wynosi zero, a współrzędne stają się nieskończone. Trójwymiarowe systemy graficzne są zbudowane wokół metafory aparatu, a depth wartość reprezentuje odległość aparatu od źródła układu współrzędnych. Jeśli obiekt graficzny ma współrzędną Z, która jest depth jednostką pochodzenia, koncepcyjnie dotyka obiektywu aparatu i staje się nieskończenie duża.

Pamiętaj, że prawdopodobnie użyjesz tej perspectiveMatrix wartości w połączeniu z macierzami obrotowymi. Jeśli obracany obiekt graficzny ma współrzędne X lub Y większe niż depth, rotacja tego obiektu w przestrzeni 3D może obejmować współrzędne Z większe niż depth. Należy tego unikać! Podczas tworzenia perspectiveMatrix chcesz ustawić depth wartość wystarczająco dużą dla wszystkich współrzędnych w obiekcie graficznym niezależnie od tego, jak jest obracana. Gwarantuje to, że nigdy nie ma żadnego podziału o zero.

Połączenie obrotu 3D i perspektywy wymaga pomnożenia macierzy 4-by-4 razem. W tym celu SKMatrix44 definiuje metody łączenia. Jeśli A obiekty i BSKMatrix44 obiektami, następujący kod ustawia wartość A równą A × B:

A.PostConcat(B);

Gdy macierz przekształcania 4-by-4 jest używana w systemie graficznym 2D, jest stosowana do obiektów 2D. Te obiekty są płaskie i zakłada się, że współrzędnych Z wynosi zero. Mnożenie transformacji jest nieco prostsze niż pokazana wcześniej transformacja:

                 |  M11  M12  M13  M14  |
| x  y  0  1 | × |  M21  M22  M23  M24  | = | x'  y'  z'  w' |
                 |  M31  M32  M33  M34  |
                 |  M41  M42  M43  M44  |

Ta wartość 0 dla z powoduje przekształcenie formuł, które nie obejmują żadnych komórek w trzecim wierszu macierzy:

x' = M11·x + M21·y + M41

y' = M12·x + M22·y + M42

z' = M13·x + M23·y + M43

w' = M14·x + M24·y + M44

Co więcej, współrzędna z również nie ma znaczenia tutaj. Gdy obiekt 3D jest wyświetlany w systemie graficznym 2D, jest zwinięty do obiektu dwuwymiarowego, ignorując wartości współrzędnych Z. Formuły przekształcania są naprawdę tylko tymi dwoma:

x" = x' / w'

y" = y' / w'

Oznacza to, że trzeci wiersz i trzecia kolumna macierzy 4-by-4 mogą być ignorowane.

Ale jeśli tak jest, dlaczego macierz 4-by-4 jest nawet niezbędna w pierwszej kolejności?

Mimo że trzeci wiersz i trzecia kolumna 4-by-4 są nieistotne dla przekształceń dwuwymiarowych, trzeci wiersz i kolumna odgrywają rolę przed tym, gdy różne SKMatrix44 wartości są mnożone razem. Załóżmy na przykład, że pomnożysz rotację wokół osi Y za pomocą przekształcenia perspektywy:

|  cos(α)  0  –sin(α)  0  |   |  1  0  0      0     |   |  cos(α)  0  –sin(α)   sin(α)/depth  |
|    0     1     0     0  | × |  0  1  0      0     | = |    0     1     0           0        |
|  sin(α)  0   cos(α)  0  |   |  0  0  1  -1/depth  |   |  sin(α)  0   cos(α)  -cos(α)/depth  |  
|    0     0     0     1  |   |  0  0  0      1     |   |    0     0     0           1        |

W produkcie komórka M14 zawiera teraz wartość perspektywy. Jeśli chcesz zastosować tę macierz do obiektów 2D, trzeci wiersz i kolumna zostaną wyeliminowane, aby przekonwertować ją na macierz 3-by-3:

|  cos(α)  0  sin(α)/depth  |
|    0     1       0        |
|    0     0       1        |

Teraz można go użyć do przekształcenia punktu 2D:

                |  cos(α)  0  sin(α)/depth  |
|  x  y  1  | × |    0     1       0        | = |  x'  y'  z'  |
                |    0     0       1        |

Formuły przekształcania to:

x' = cos(α)·x

y' = y

z' = (sin(α)/depth)·x + 1

Teraz podziel wszystko przez z':

x" = cos(α)·x / ((sin(α)/depth)·x + 1)

y" = y / ((sin(α)/depth)·x + 1)

Gdy obiekty 2D są obracane z dodatnim kątem wokół osi Y, wartości dodatnie X cofają się do tła, podczas gdy ujemne wartości X przychodzą na pierwszy plan. Wartości X wydają się zbliżać do osi Y (która jest określana przez wartość cosinus) jako współrzędne najdalej z osi Y staje się mniejsza lub większa, gdy przesuwają się dalej z przeglądarki lub bliżej osoby przeglądającego.

W przypadku korzystania z programu SKMatrix44wykonaj wszystkie operacje obrotu 3D i perspektywy, mnożąc różne SKMatrix44 wartości. Następnie można wyodrębnić dwuwymiarową macierz 3-by-3 z macierzy 4-by-4 przy użyciu Matrix właściwości SKMatrix44 klasy . Ta właściwość zwraca znaną SKMatrix wartość.

Strona Rotacja 3D umożliwia eksperymentowanie z rotacją 3D . Plik Rotation3DPage.xaml tworzy cztery suwaki, aby ustawić obrót wokół osi X, Y i Z oraz ustawić wartość głębokości:

<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.Rotation3DPage"
             Title="Rotation 3D">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <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="Margin" Value="20, 0" />
                    <Setter Property="Maximum" Value="360" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="xRotateSlider"
                Grid.Row="0"
                ValueChanged="OnSliderValueChanged" />

        <Label Text="{Binding Source={x:Reference xRotateSlider},
                              Path=Value,
                              StringFormat='X-Axis Rotation = {0:F0}'}"
               Grid.Row="1" />

        <Slider x:Name="yRotateSlider"
                Grid.Row="2"
                ValueChanged="OnSliderValueChanged" />

        <Label Text="{Binding Source={x:Reference yRotateSlider},
                              Path=Value,
                              StringFormat='Y-Axis Rotation = {0:F0}'}"
               Grid.Row="3" />

        <Slider x:Name="zRotateSlider"
                Grid.Row="4"
                ValueChanged="OnSliderValueChanged" />

        <Label Text="{Binding Source={x:Reference zRotateSlider},
                              Path=Value,
                              StringFormat='Z-Axis Rotation = {0:F0}'}"
               Grid.Row="5" />

        <Slider x:Name="depthSlider"
                Grid.Row="6"
                Maximum="2500"
                Minimum="250"
                ValueChanged="OnSliderValueChanged" />

        <Label Grid.Row="7"
               Text="{Binding Source={x:Reference depthSlider},
                              Path=Value,
                              StringFormat='Depth = {0:F0}'}" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="8"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

Zwróć uwagę, że element depthSlider jest inicjowany z wartością Minimum 250. Oznacza to, że obracany w tym miejscu obiekt 2D ma współrzędne X i Y ograniczone do okręgu zdefiniowanego przez promień 250 pikseli wokół źródła. Każda rotacja tego obiektu w przestrzeni 3D zawsze spowoduje współrzędnych wartości mniejszych niż 250.

Plik Rotation3DPage.cs za pomocą kodu ładuje się w mapie bitowej, która ma 300 pikseli kwadratowych:

public partial class Rotation3DPage : ContentPage
{
    SKBitmap bitmap;

    public Rotation3DPage()
    {
        InitializeComponent();

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

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

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        if (canvasView != null)
        {
            canvasView.InvalidateSurface();
        }
    }
    ...
}

Jeśli transformacja 3D jest wyśrodkowana na tej mapie bitowej, współrzędne X i Y wahają się od –150 do 150, podczas gdy narożniki są 212 pikseli od środka, więc wszystko znajduje się w promieniu 250 pikseli.

Procedura PaintSurface obsługi tworzy SKMatrix44 obiekty na podstawie suwaków i mnoży je razem przy użyciu polecenia PostConcat. SKMatrix Wartość wyodrębniona z obiektu końcowego SKMatrix44 jest otoczona przetłumaczeniami, aby wyśrodkować obrót w środku ekranu:

public partial class Rotation3DPage : ContentPage
{
    SKBitmap bitmap;

    public Rotation3DPage()
    {
        InitializeComponent();

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

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

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        if (canvasView != null)
        {
            canvasView.InvalidateSurface();
        }
    }

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

        canvas.Clear();

        // Find center of canvas
        float xCenter = info.Width / 2;
        float yCenter = info.Height / 2;

        // Translate center to origin
        SKMatrix matrix = SKMatrix.MakeTranslation(-xCenter, -yCenter);

        // Use 3D matrix for 3D rotations and perspective
        SKMatrix44 matrix44 = SKMatrix44.CreateIdentity();
        matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(1, 0, 0, (float)xRotateSlider.Value));
        matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 1, 0, (float)yRotateSlider.Value));
        matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 0, 1, (float)zRotateSlider.Value));

        SKMatrix44 perspectiveMatrix = SKMatrix44.CreateIdentity();
        perspectiveMatrix[3, 2] = -1 / (float)depthSlider.Value;
        matrix44.PostConcat(perspectiveMatrix);

        // Concatenate with 2D matrix
        SKMatrix.PostConcat(ref matrix, matrix44.Matrix);

        // Translate back to center
        SKMatrix.PostConcat(ref matrix,
            SKMatrix.MakeTranslation(xCenter, yCenter));

        // Set the matrix and display the bitmap
        canvas.SetMatrix(matrix);
        float xBitmap = xCenter - bitmap.Width / 2;
        float yBitmap = yCenter - bitmap.Height / 2;
        canvas.DrawBitmap(bitmap, xBitmap, yBitmap);
    }
}

Podczas eksperymentowania z czwartym suwakiem zauważysz, że różne ustawienia głębokości nie przenoszą obiektu dalej od przeglądarki, ale zmieniają zakres efektu perspektywy:

Potrójny zrzut ekranu przedstawiający stronę Rotacja 3D

Animowany obrót 3D używa SKMatrix44 również do animowania ciągu tekstowego w przestrzeni 3D. Obiekt textPaint ustawiony jako pole jest używany w konstruktorze w celu określenia granic tekstu:

public class AnimatedRotation3DPage : ContentPage
{
    SKCanvasView canvasView;
    float xRotationDegrees, yRotationDegrees, zRotationDegrees;
    string text = "SkiaSharp";
    SKPaint textPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black,
        TextSize = 100,
        StrokeWidth = 3,
    };
    SKRect textBounds;

    public AnimatedRotation3DPage()
    {
        Title = "Animated Rotation 3D";

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

        // Measure the text
        textPaint.MeasureText(text, ref textBounds);
    }
    ...
}

Przesłonięcia OnAppearing definiują trzy Xamarin.FormsAnimation obiekty, aby animować xRotationDegreespola , yRotationDegreesi zRotationDegrees w różnych stawkach. Zwróć uwagę, że okresy tych animacji są ustawione na liczby pierwsze (5 sekund, 7 sekund i 11 sekund), więc ogólna kombinacja powtarza się tylko co 385 sekund lub więcej niż 10 minut:

public class AnimatedRotation3DPage : ContentPage
{
    ...
    protected override void OnAppearing()
    {
        base.OnAppearing();

        new Animation((value) => xRotationDegrees = 360 * (float)value).
            Commit(this, "xRotationAnimation", length: 5000, repeat: () => true);

        new Animation((value) => yRotationDegrees = 360 * (float)value).
            Commit(this, "yRotationAnimation", length: 7000, repeat: () => true);

        new Animation((value) =>
        {
            zRotationDegrees = 360 * (float)value;
            canvasView.InvalidateSurface();
        }).Commit(this, "zRotationAnimation", length: 11000, repeat: () => true);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        this.AbortAnimation("xRotationAnimation");
        this.AbortAnimation("yRotationAnimation");
        this.AbortAnimation("zRotationAnimation");
    }
    ...
}

Podobnie jak w poprzednim programie program PaintCanvas obsługi tworzy SKMatrix44 wartości dla rotacji i perspektywy i mnoży je razem:

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

        canvas.Clear();

        // Find center of canvas
        float xCenter = info.Width / 2;
        float yCenter = info.Height / 2;

        // Translate center to origin
        SKMatrix matrix = SKMatrix.MakeTranslation(-xCenter, -yCenter);

        // Scale so text fits
        float scale = Math.Min(info.Width / textBounds.Width,
                               info.Height / textBounds.Height);
        SKMatrix.PostConcat(ref matrix, SKMatrix.MakeScale(scale, scale));

        // Calculate composite 3D transforms
        float depth = 0.75f * scale * textBounds.Width;

        SKMatrix44 matrix44 = SKMatrix44.CreateIdentity();
        matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(1, 0, 0, xRotationDegrees));
        matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 1, 0, yRotationDegrees));
        matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 0, 1, zRotationDegrees));

        SKMatrix44 perspectiveMatrix = SKMatrix44.CreateIdentity();
        perspectiveMatrix[3, 2] = -1 / depth;
        matrix44.PostConcat(perspectiveMatrix);

        // Concatenate with 2D matrix
        SKMatrix.PostConcat(ref matrix, matrix44.Matrix);

        // Translate back to center
        SKMatrix.PostConcat(ref matrix,
            SKMatrix.MakeTranslation(xCenter, yCenter));

        // Set the matrix and display the text
        canvas.SetMatrix(matrix);
        float xText = xCenter - textBounds.MidX;
        float yText = yCenter - textBounds.MidY;
        canvas.DrawText(text, xText, yText, textPaint);
    }
}

Ta rotacja 3D jest otoczona kilkoma transformacjami 2D, aby przenieść środek obrotu do środka ekranu i skalować rozmiar ciągu tekstowego, tak aby była taka sama szerokość jak ekran:

Potrójny zrzut ekranu przedstawiający stronę Animowana rotacja 3D