Condividi tramite


Trasformazioni non affini

Creare effetti prospettica e taper con la terza colonna della matrice di trasformazione

La traslazione, la scalabilità, la rotazione e l'asimmetria sono tutte classificate come trasformazioni affine . Le trasformazioni affine mantengono le linee parallele. Se due righe sono parallele prima della trasformazione, rimangono parallele dopo la trasformazione. I rettangoli vengono sempre trasformati in parallelogrammi.

Tuttavia, SkiaSharp è anche in grado di trasformare un rettangolo in qualsiasi quadrilatero convesso:

Bitmap trasformata in quadrilatero convessa

Un quadrilatero convessa è una figura a quattro lati con angoli interni sempre inferiori a 180 gradi e lati che non si incrociano tra loro.

Le trasformazioni non affine vengono restituite quando la terza riga della matrice di trasformazione è impostata su valori diversi da 0, 0 e 1. La moltiplicazione completa SKMatrix è:

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

Le formule di trasformazione risultanti sono:

x' = ScaleX·x + SkewX·y + TransX

y' = asimmetriaY·x + ScaleY·y + TransY

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

La regola fondamentale dell'uso di una matrice 3 per 3 per le trasformazioni bidimensionali è che tutto rimane sul piano in cui Z è uguale a 1. A meno che Persp0 e Persp1 non siano 0 e Persp2 uguale a 1, la trasformazione ha spostato le coordinate Z al di fuori del piano.

Per ripristinarlo in una trasformazione bidimensionale, le coordinate devono essere spostate di nuovo in tale piano. È necessario un altro passaggio. I valori x', y' e z' devono essere divisi per z':

x" = x' / z'

y" = y' / z'

z" = z' / z' = 1

Questi sono noti come coordinate omogenee e sono stati sviluppati dal matematico August August Möbius, molto meglio noto per la sua stranezza topologica, la Striscia Möbius.

Se z' è 0, la divisione genera coordinate infinite. Infatti, una delle motivazioni di Möbius per lo sviluppo di coordinate omogenee era la capacità di rappresentare valori infiniti con numeri finiti.

Quando si visualizzano elementi grafici, tuttavia, si vuole evitare di eseguire il rendering di elementi con coordinate che si trasformano in valori infiniti. Il rendering di tali coordinate non verrà eseguito. Tutto nella vicinanza di tali coordinate sarà molto grande e probabilmente non coerente visivamente.

In questa equazione non si vuole che il valore di z diventi zero:

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

Di conseguenza, questi valori presentano alcune restrizioni pratiche:

La Persp2 cella può essere zero o non zero. Se Persp2 è zero, z' è zero per il punto (0, 0) e questo in genere non è auspicabile perché questo punto è molto comune nella grafica bidimensionale. Se Persp2 non è uguale a zero, non vi è alcuna perdita di generalità se Persp2 è fissa a 1. Ad esempio, se si determina che Persp2 deve essere 5, è possibile dividere semplicemente tutte le celle nella matrice per 5, che rende Persp2 uguale a 1 e il risultato sarà lo stesso.

Per questi motivi, Persp2 è spesso fisso a 1, che corrisponde allo stesso valore nella matrice identity.

In genere, Persp0 e Persp1 sono numeri piccoli. Si supponga, ad esempio, di iniziare con una matrice di identità ma impostata su Persp0 0.01:

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

Le formule di trasformazione sono:

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

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

Usare ora questa trasformazione per eseguire il rendering di una casella quadrata di 100 pixel posizionata all'origine. Ecco come vengono trasformati i quattro angoli:

(0, 0) → (0, 0)

(0, 100) → (0, 100)

(100, 0) → (50, 0)

(100, 100) → (50, 50)

Quando x è 100, il denominatore z è 2, quindi le coordinate x e y vengono effettivamente dimezzate. Il lato destro della casella diventa più breve del lato sinistro:

Una scatola sottoposta a una trasformazione non affine

La Persp parte di questi nomi di cella si riferisce a "prospettiva" perché la foreshortening suggerisce che la casella è ora inclinata con il lato destro più lontano dal visualizzatore.

La pagina Prospettiva di test consente di sperimentare i valori di Persp0 e Pers1 di ottenere un'esperienza di funzionamento. I valori ragionevoli di queste celle di matrice sono così piccoli che l'oggetto Slider nel piattaforma UWP (Universal Windows Platform) non può gestirli correttamente. Per risolvere il problema della piattaforma UWP, è necessario inizializzare i due Slider elementi in TestPerspective.xaml per variare da -1 a 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>

I gestori eventi per i dispositivi di scorrimento nel TestPerspectivePage file code-behind dividono i valori per 100 in modo che siano compresi tra -0,01 e 0,01. Inoltre, il costruttore carica in una bitmap:

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

Il PaintSurface gestore calcola un SKMatrix valore denominato perspectiveMatrix in base ai valori di questi due dispositivi di scorrimento divisi per 100. Questa operazione viene combinata con due trasformazioni di traslazione che inseriscono il centro di questa trasformazione al centro della bitmap:

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

Ecco alcune immagini di esempio:

Screenshot triplo della pagina Prospettiva di test

Mentre si sperimentano i dispositivi di scorrimento, si scoprirà che i valori superiori a 0,0066 o inferiori a –0,0066 causano improvvisamente la frattura e l'incoerente dell'immagine. La bitmap da trasformare è un quadrato di 300 pixel. Viene trasformato rispetto al centro, quindi le coordinate dell'intervallo bitmap da -150 a 150. Tenere presente che il valore di z' è:

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

Se Persp0 o Persp1 è maggiore di 0,0066 o inferiore a –0,0066, esiste sempre una coordinata della bitmap che restituisce il valore z di zero. Ciò causa la divisione per zero e il rendering diventa un casino. Quando si usano trasformazioni non affine, si vuole evitare di eseguire il rendering di qualsiasi elemento con coordinate che causano la divisione per zero.

In genere, non verrà impostato Persp0 e Persp1 in isolamento. Spesso è anche necessario impostare altre celle nella matrice per ottenere determinati tipi di trasformazioni non affine.

Una di queste trasformazioni non affine è una trasformazione taper. Questo tipo di trasformazione non affine mantiene le dimensioni complessive di un rettangolo, ma tocca un lato:

Una scatola sottoposta a una trasformazione taper

La TaperTransform classe esegue un calcolo generalizzato di una trasformazione non affine in base a questi parametri:

  • la dimensione rettangolare dell'immagine da trasformare,
  • enumerazione che indica il lato del rettangolo che tocca,
  • un'altra enumerazione che indica la modalità di tocco e
  • l'estensione del nastro.

Ecco il codice:

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

Questa classe viene usata nella pagina Trasformazione taper. Il file XAML crea un'istanza di due Picker elementi per selezionare i valori di enumerazione e un oggetto Slider per la scelta della frazione del taper. Il PaintSurface gestore combina la trasformazione del taper con due trasformazioni convertite per rendere la trasformazione relativa all'angolo superiore sinistro della bitmap:

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

Di seguito sono riportati alcuni esempi.

Screenshot triplo della pagina Trasformazione taper

Un altro tipo di trasformazioni non affine generalizzate è la rotazione 3D, illustrata nell'articolo successivo, rotazioni 3D.

La trasformazione non affine può trasformare un rettangolo in qualsiasi quadrilatero convesso. Ciò è dimostrato dalla pagina Mostra matrice non affine. È molto simile alla pagina Mostra matrice affine dell'articolo Trasformazioni matrice, ad eccezione del fatto che ha un quarto oggetto per modificare il quarto TouchPoint angolo della bitmap:

Screenshot triplo della pagina Mostra matrice non affine

Purché non si tenti di creare un angolo interno di uno degli angoli della bitmap maggiore di 180 gradi o che due lati si incrocino tra loro, il programma calcola correttamente la trasformazione usando questo metodo dalla ShowNonAffineMatrixPage classe :

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

Per semplificare il calcolo, questo metodo ottiene la trasformazione totale come prodotto di tre trasformazioni separate, che vengono simboleggiate qui con frecce che mostrano come queste trasformazioni modificano i quattro angoli della bitmap:

(0, 0) → (0, 0) → (0, 0) → (x0, y0) (in alto a sinistra)

(0, H) → (0, 1) → (0, 1) → (x1, y1) (in basso a sinistra)

(W, 0) → (1, 0) → (1, 0) → (x2, y2) (in alto a destra)

(W, H) → (1, 1) → (a, b) → (x3, y3) (in basso a destra)

Le coordinate finali a destra sono i quattro punti associati ai quattro punti di tocco. Queste sono le coordinate finali degli angoli della bitmap.

W e H rappresentano la larghezza e l'altezza della bitmap. La prima trasformazione S ridimensiona semplicemente la bitmap in un quadrato di 1 pixel. La seconda trasformazione è la trasformazione Nnon affine e la terza è la trasformazione Aaffine . Tale trasformazione affine si basa su tre punti, quindi è proprio come il metodo affine ComputeMatrix precedente e non implica la quarta riga con il punto (a, b).

I a valori e b vengono calcolati in modo che la terza trasformazione sia affine. Il codice ottiene l'inverso della trasformazione affine e quindi lo usa per eseguire il mapping dell'angolo inferiore destro. Questo è il punto (a, b).

Un altro uso di trasformazioni non affine consiste nell'simulare grafica tridimensionale. Nell'articolo successivo, rotazioni 3D si vedrà come ruotare un elemento grafico bidimensionale nello spazio 3D.