Condividi tramite


Rotazioni 3D in SkiaSharp

Usa trasformazioni non affine per ruotare oggetti 2D nello spazio 3D.

Un'applicazione comune delle trasformazioni non affine consiste nel simulare la rotazione di un oggetto 2D nello spazio 3D:

Stringa di testo ruotata nello spazio 3D

Questo processo comporta l'uso di rotazioni tridimensionali e quindi la derivazione di una trasformazione non affine SKMatrix che esegue queste rotazioni 3D.

È difficile sviluppare questa SKMatrix trasformazione solo all'interno di due dimensioni. Il processo diventa molto più semplice quando questa matrice 3 per 3 è derivata da una matrice 4-by-4 usata nella grafica 3D. SkiaSharp include la SKMatrix44 classe a questo scopo, ma alcuni sfondi nella grafica 3D sono necessari per comprendere le rotazioni 3D e la matrice di trasformazione da 4 a 4.

Un sistema di coordinate tridimensionale aggiunge un terzo asse denominato Z. Concettualmente, l'asse Z si trova ad angoli giusti sullo schermo. I punti di coordinate nello spazio 3D sono indicati con tre numeri: (x, y, z). Nel sistema di coordinate 3D usato in questo articolo, l'aumento dei valori di X è a destra e l'aumento dei valori di Y scende, proprio come in due dimensioni. L'aumento dei valori Z positivi esce dallo schermo. L'origine è l'angolo superiore sinistro, proprio come nella grafica 2D. Si può pensare allo schermo come un piano XY con l'asse Z ad angoli giusti per questo piano.

Si tratta di un sistema di coordinate a sinistra. Se punti l'indicatore di avanzamento per la mano sinistra nella direzione delle coordinate X positive (a destra) e il dito medio nella direzione di aumentare le coordinate Y (giù), il pollice punta nella direzione di aumentare le coordinate Z , estendendosi dallo schermo.

Nella grafica 3D le trasformazioni si basano su una matrice 4 per 4. Ecco la matrice di identità da 4 a 4:

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

Nell'uso di una matrice da 4 a 4, è utile identificare le celle con i relativi numeri di riga e colonna:

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

Tuttavia, la classe SkiaSharp Matrix44 è leggermente diversa. L'unico modo per impostare o ottenere singoli valori di cella in SKMatrix44 consiste nell'usare l'indicizzatore Item . Gli indici di riga e colonna sono in base zero anziché in base uno e le righe e le colonne vengono scambiate. È possibile accedere alla cella M14 nel diagramma precedente usando l'indicizzatore [3, 0] in un SKMatrix44 oggetto .

In un sistema di grafica 3D, un punto 3D (x, y, z) viene convertito in una matrice da 1 a 4 per moltiplicare per la matrice di trasformazione da 4 a 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  |

Analogamente alle trasformazioni 2D che si verificano in tre dimensioni, si presuppone che le trasformazioni 3D vengano eseguite in quattro dimensioni. La quarta dimensione viene definita W e si presuppone che lo spazio 3D esista all'interno dello spazio 4D in cui le coordinate W sono uguali a 1. Le formule di trasformazione sono le seguenti:

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

È ovvio dalle formule di trasformazione che le celle M11, M22sono M33 fattori di ridimensionamento nelle direzioni X, Y e Z e , M42e M43M41sono fattori di traslazione nelle direzioni X, Y e Z.

Per convertire di nuovo queste coordinate nello spazio 3D in cui W è uguale a 1, le coordinate x', y' e z' sono tutte divise per w':

x" = x' / w'

y" = y' / w'

z" = z' / w'

w" = w' / w' = 1

Tale divisione per w' offre una prospettiva nello spazio 3D. Se w' è uguale a 1, non si verifica alcuna prospettiva.

Le rotazioni nello spazio 3D possono essere piuttosto complesse, ma le rotazioni più semplici sono quelle intorno agli assi X, Y e Z. Una rotazione dell'angolo α intorno all'asse X è la matrice seguente:

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

I valori di X rimangono invariati se sottoposti a questa trasformazione. La rotazione intorno all'asse Y lascia invariati i valori di Y:

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

La rotazione attorno all'asse Z è identica a quella della grafica 2D:

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

La direzione di rotazione è implicita dalla mano del sistema di coordinate. Si tratta di un sistema sinistrorso, quindi se si punta il pollice della mano sinistra verso l'aumento dei valori per un particolare asse, verso destra per la rotazione intorno all'asse X, verso il basso per la rotazione intorno all'asse Y e verso di voi per la rotazione intorno all'asse Z, allora la curva delle altre dita indica la direzione di rotazione per gli angoli positivi.

SKMatrix44dispone di metodi e CreateRotationDegrees statici generalizzati CreateRotation che consentono di specificare l'asse intorno al quale si verifica la rotazione:

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

Per la rotazione intorno all'asse X, impostare i primi tre argomenti su 1, 0, 0. Per la rotazione attorno all'asse Y, impostarli su 0, 1, 0 e per la rotazione attorno all'asse Z, impostarli su 0, 0, 1.

La quarta colonna della 4 per 4 è per la prospettiva. Non SKMatrix44 dispone di metodi per la creazione di trasformazioni prospettiche, ma è possibile crearne uno usando il codice seguente:

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

Il motivo del nome depth dell'argomento sarà evidente a breve. Il codice crea la matrice:

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

Le formule di trasformazione comportano il calcolo seguente di w':

w' = –z / depth + 1

Ciò serve a ridurre le coordinate X e Y quando i valori di Z sono inferiori a zero (concettualmente dietro il piano XY) e aumentare le coordinate X e Y per i valori positivi di Z. Quando la coordinata Z è uguale deptha , w' è zero e le coordinate diventano infinite. I sistemi grafici tridimensionali sono costruiti intorno a una metafora della fotocamera e il depth valore qui rappresenta la distanza della fotocamera dall'origine del sistema di coordinate. Se un oggetto grafico ha una coordinata Z che è depth unità dall'origine, sta toccando concettualmente l'obiettivo della fotocamera e diventa infinitamente grande.

Tenere presente che probabilmente si userà questo perspectiveMatrix valore in combinazione con le matrici di rotazione. Se un oggetto grafico ruotato ha coordinate X o Y maggiori di depth, è probabile che la rotazione di questo oggetto nello spazio 3D includa coordinate Z maggiori di depth. Questo deve essere evitato! Quando si crea perspectiveMatrix si desidera impostare depth un valore sufficientemente grande per tutte le coordinate nell'oggetto grafico, indipendentemente dalla modalità di rotazione. In questo modo si garantisce che non vi sia mai alcuna divisione per zero.

La combinazione di rotazioni 3D e prospettiva richiede la moltiplicazione di matrici da 4 a 4. A questo scopo, SKMatrix44 definisce i metodi di concatenazione. Se A e B sono SKMatrix44 oggetti, il codice seguente imposta A uguale a A × B:

A.PostConcat(B);

Quando una matrice di trasformazione 4 per 4 viene usata in un sistema di grafica 2D, viene applicata agli oggetti 2D. Questi oggetti sono flat e si presuppone che abbiano coordinate Z pari a zero. La moltiplicazione della trasformazione è leggermente più semplice della trasformazione illustrata in precedenza:

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

Tale valore 0 per z comporta formule di trasformazione che non coinvolgono celle nella terza riga della matrice:

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

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

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

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

Inoltre, la coordinata z è irrilevante anche qui. Quando un oggetto 3D viene visualizzato in un sistema di grafica 2D, viene compresso in un oggetto bidimensionale ignorando i valori delle coordinate Z. Le formule di trasformazione sono proprio queste due:

x" = x' / w'

y" = y' / w'

Ciò significa che è possibile ignorare la terza riga e la terza colonna della matrice 4 by-4.

Ma se è così, perché è la matrice 4 per 4 anche necessaria al primo posto?

Anche se la terza riga e la terza colonna della 4-per-4 sono irrilevanti per le trasformazioni bidimensionali, la terza riga e la terza colonna svolgono un ruolo prima che i vari SKMatrix44 valori vengano moltiplicati insieme. Si supponga, ad esempio, di moltiplicare la rotazione attorno all'asse Y con la trasformazione prospettica:

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

Nel prodotto la cella M14 contiene ora un valore prospettica. Se si desidera applicare tale matrice a oggetti 2D, la terza riga e colonna vengono eliminate per convertirla in una matrice da 3 a 3:

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

Ora può essere usato per trasformare un punto 2D:

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

Le formule di trasformazione sono:

x' = cos(α)·x

y' = y

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

Dividere tutto per z':

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

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

Quando gli oggetti 2D vengono ruotati con un angolo positivo attorno all'asse Y, i valori X positivi vengono spostati sullo sfondo mentre i valori X negativi vengono visualizzati in primo piano. I valori X sembrano avvicinarsi all'asse Y (che è governato dal valore del coseno) in quanto le coordinate più lontane dall'asse Y diventano più piccole o più grandi man mano che si spostano più lontano dal visualizzatore o più vicino al visualizzatore.

Quando si usa SKMatrix44, eseguire tutte le operazioni di rotazione e prospettiva 3D moltiplicando vari SKMatrix44 valori. È quindi possibile estrarre una matrice bidimensionale 3 per 3 dalla matrice 4-by-4 usando la Matrix proprietà della SKMatrix44 classe . Questa proprietà restituisce un valore familiare SKMatrix .

La pagina Rotation 3D consente di sperimentare la rotazione 3D. Il file Rotation3DPage.xaml crea un'istanza di quattro dispositivi di scorrimento per impostare la rotazione intorno agli assi X, Y e Z e per impostare un valore di profondità:

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

Si noti che l'oggetto depthSlider viene inizializzato con un Minimum valore pari a 250. Ciò implica che l'oggetto 2D ruotato qui ha coordinate X e Y limitate a un cerchio definito da un raggio di 250 pixel intorno all'origine. Qualsiasi rotazione di questo oggetto nello spazio 3D comporterà sempre valori di coordinate inferiori a 250.

Il file code-behind Rotation3DPage.cs viene caricato in una bitmap con 300 pixel quadrati:

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

Se la trasformazione 3D è centrata su questa bitmap, le coordinate X e Y sono comprese tra –150 e 150, mentre gli angoli sono 212 pixel dal centro, quindi tutto è compreso nel raggio di 250 pixel.

Il PaintSurface gestore crea SKMatrix44 oggetti in base ai dispositivi di scorrimento e li moltiplica insieme usando PostConcat. Il SKMatrix valore estratto dall'oggetto finale SKMatrix44 è circondato da trasformazioni convertite al centro della rotazione al centro dello schermo:

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

Quando si prova con il quarto dispositivo di scorrimento, si noterà che le diverse impostazioni di profondità non spostano l'oggetto più lontano dal visualizzatore, ma modificano invece l'estensione dell'effetto prospettica:

Screenshot triplo della pagina Rotazione 3D

La rotazione animata 3D usa SKMatrix44 anche per animare una stringa di testo nello spazio 3D. Il textPaint set di oggetti come campo viene utilizzato nel costruttore per determinare i limiti del testo:

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

L'override OnAppearing definisce tre Xamarin.FormsAnimation oggetti per animare i xRotationDegreescampi , yRotationDegreese zRotationDegrees a frequenze diverse. Si noti che i periodi di queste animazioni sono impostati su numeri primi (5 secondi, 7 secondi e 11 secondi), quindi la combinazione complessiva si ripete solo ogni 385 secondi o più di 10 minuti:

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

Come nel programma precedente, il gestore crea SKMatrix44 valori per la rotazione e la PaintCanvas prospettiva e li moltiplica insieme:

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

Questa rotazione 3D è circondata da diverse trasformazioni 2D per spostare il centro di rotazione al centro dello schermo e per ridimensionare le dimensioni della stringa di testo in modo che sia la stessa larghezza dello schermo:

Screenshot triplo della pagina Rotazione animata 3D