Трехмерные матричные преобразования в WinRT

145

А теперь мы займемся неприятным делом - математикой. Как вы уже знаете, двумерная графика требует использования матрицы преобразования 3x3 для поддержки сдвига наряду с масштабированием, поворотом и отклонением. Концептуально точка (x,y) рассматривается как существующая в трехмерном пространстве с координатами (x, y, 1).

Применение обобщенного двумерного аффинного преобразования выглядит так:

Это фактические значения полей используемой структуры Matrix. Фиксированный третий столбец ограничивает матрицу аффинными преобразованиями. Из применения матричного умножения вытекают следующие формулы преобразования:

Так как преобразование является аффинным, квадрат всегда преобразуется в параллелограмм. Этот параллелограмм определяется тремя углами, а четвертый угол однозначно вычисляется по первым трем.

Возможно ли построить аффинное преобразование, которое будет отображать единичный квадрат на произвольный параллелограмм? По сути, требуется преобразование следующего вида:

Подставляя эти точки в формулы преобразований, мы легко приходим к следующим значениям ячеек матрицы:

В трехмерной графике потребуется матрица преобразования 4x4, а точка (x, y, z) интерпретируется как существующая в четырехмерном пространстве с координатами (x, y, z, 1). Так как после z букв уже нет, четвертое измерение обычно обозначается буквой w. Применение преобразования выглядит так:

Это фактические значения полей используемой структуры Matrix3D.

Затем полученная матрица 4x1 преобразуется в точку трехмерного пространства, для чего координаты делятся на w':

В традиционной двумерной графике возможность деления на нуль обычно нежелательна. Но в трехмерной графике деление на значение, которое может быть равно нулю, играет очень важную роль, потому что оно используется для реализации перспективы. Параллельные линии должны не встречаться в бесконечности.

Структура Matrix3D в Windows Runtime используется только для задания свойства ProjectionMatrix объекта Matrix3DProjection, который затем может быть задан свойству Projection элемента (вместо использования PlaneProjection). В коде XAML это может выглядеть так:

<Image Source="...">
    <Image.Projection>
        <Matrix3DProjection>
            <Matrix3DProjection.ProjectionMatrix>
        1 0 0 0, 0 1 0 0,
        0 0 1 0, 0 0 0 1
            </Matrix3DProjection.ProjectionMatrix>
        </Matrix3DProjection>
    </Image.Projection>
</Image>

Непосредственно создать экземпляр Matrix3D в XAML нельзя; вместо этого необходимо задать 16 чисел, определяющих матрицу, начиная с первой строки. В примере приведена матрица тождественного преобразования с характерной диагональю 1.

Полноценная матрица 4x4 не используется в этом контексте, потому что у элемента, к которому она применяется, координата Z равна 0, поэтому применение матрицы в действительности выглядит следующим образом:

Это означает, что ячейки, образующие всю третью строку - M31, M32, M33 и M34, - несущественны. Они умножаются на 0, а следовательно, не участвуют в вычислениях.

Более того, трехмерная точка, вычисленная в результате этого процесса, проходит свертку по оси Z для получения двумерной точки для отображения на экране монитора:

Этот процесс выполняется и в стандартной трехмерной графике, но обычно он требует существенно больше работы, потому что значения Z также определяют, какие части изображения видны с позиции камеры, а какие остаются скрытыми.

Более того, в стандартной трехмерной графике поддерживается только ограниченный диапазон значений Z. В координатах Z определяются «ближняя плоскость» и «дальняя плоскость», и видимыми являются только координаты, находящиеся между этими двумя плоскостями. Остальные значения игнорируются, потому что они концептуально расположены слишком близко или слишком далеко от камеры. В Windows Runtime учитываются только координаты со значениями Z от 0 до 1. Чтобы избежать потери части преобразованного элемента, M13 и M23 должны быть равны нулю. Ячейке OffsetZ можно задать произвольное значение от 0 до 1, но обычно также удобно занести в нее нуль.

Следовательно, при применении Matrix3DProjection к двумерному элементу формулы преобразования принимают вид:

Если ячейки M14 и M24 равны нулю, а ячейка M44 равна 1, получается простое двумерное аффинное преобразование. Ненулевые значения M14 и M24 являются неаффинными частями формул. Значение M44 может быть отлично от 1, но если оно не равно нулю, всегда можно найти эквивалентное преобразование, в котором M44 = 1 - достаточно умножить все поля на 1 / M44.

При использовании неаффинного преобразования квадрат не обязательно преобразуется в параллелограмм. Однако у матрицы неаффинного преобразования тоже имеются свои ограничения: оно не может преобразовать квадрат в произвольный четырехугольник. Преобразованные линии не могут пересекать друг друга, а четыре угла должны быть выпуклыми.

Попробуем определить неаффинное преобразование, которое отображает четыре угла квадрата на четыре произвольные точки:

Чтобы упростить задачу, мы разобьем ее на два преобразования:

Первое преобразование очевидно является неаффинным; я назову его B. Второе преобразование, которое мы сделаем аффинным, называется A (от Affine). Чтобы сделать преобразование аффинным, мы вычислим соответствующие значения a и b. Составное преобразование имеет вид B x A.

Я уже показал, как вычисляется аффинное преобразование, и переход от матрицы 3x3 к матрице 4x4 даже не требует изменения формы записи. Однако мы также хотим чтобы аффинное преобразование отображало точку (a, b) на произвольную точку (x3, y3). Применяя вычисленное аффинное преобразование к (a, b) и решая систему уравнений для a и b, мы получаем:

Теперь займемся неаффинным преобразованием, которое должно обеспечивать следующие отображения:

А вот формулы преобразования, приведенные ранее:

Помните, что x' и y' необходимо разделить на w' для получения преобразованной точки.

Если (0,0) отображается на (0,0), то OffsetX и OffsetY равны нулю, а значение M44 отлично от нуля. Давайте не будем сдерживаться и зададим M44 равным 1.

Если (0,1) отображается на (0,1), то значение M21 должно быть равно нулю (для вычисления нулевого значения x'), а результат деления y' на w' должен быть равен 1; это означает, что значение M24 равно M22 - 1.

Если (1, 0) отображается на (1, 0), то значение M12 равно нулю (для нулевого значения y') а результат деления x' на w' должен быть равен 1, то есть значение Ml4 равно M11 - 1.

Если (1,1) отображается на (a, b), то после некоторых алгебраических вычислении мы получаем:

А a и b мы уже вычислили ранее.

Теперь давайте все это запрограммируем. Программа должна выводить матрицу, полученную в этом процессе. Для этой цели будет использоваться класс DisplayMatrix3D, производный от UserControl. Файл XAML по сути не содержит ничего, кроме панели Grid с матрицей из 4x4 элементов TextBlock:

<UserControl ...>

    <UserControl.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="TextAlignment" Value="Right" />
            <Setter Property="Margin" Value="6 0" />
        </Style>
    </UserControl.Resources>

    <Border BorderBrush="#DEFFFFFF" BorderThickness="1 0">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <TextBlock Name="m11" Grid.Row="0" Grid.Column="0" />
            <TextBlock Name="m12" Grid.Row="0" Grid.Column="1" />
            <TextBlock Name="m13" Grid.Row="0" Grid.Column="2" />
            <TextBlock Name="m14" Grid.Row="0" Grid.Column="3" />

            <TextBlock Name="m21" Grid.Row="1" Grid.Column="0" />
            <TextBlock Name="m22" Grid.Row="1" Grid.Column="1" />
            <TextBlock Name="m23" Grid.Row="1" Grid.Column="2" />
            <TextBlock Name="m24" Grid.Row="1" Grid.Column="3" />

            <TextBlock Name="m31" Grid.Row="2" Grid.Column="0" />
            <TextBlock Name="m32" Grid.Row="2" Grid.Column="1" />
            <TextBlock Name="m33" Grid.Row="2" Grid.Column="2" />
            <TextBlock Name="m34" Grid.Row="2" Grid.Column="3" />

            <TextBlock Name="m41" Grid.Row="3" Grid.Column="0" />
            <TextBlock Name="m42" Grid.Row="3" Grid.Column="1" />
            <TextBlock Name="m43" Grid.Row="3" Grid.Column="2" />
            <TextBlock Name="m44" Grid.Row="3" Grid.Column="3" />
        </Grid>
    </Border>
</UserControl>

Файл фонового кода определяет свойство зависимости типа Matrix3D, чтобы получать оповещение при каждом изменении свойства. Будьте внимательны: оповещение не поступит при изменении свойства существующей структуры Matrix3D. Заменяться должна вся структура полностью.

using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Media3D;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class DisplayMatrix3D : UserControl
    {
        static DependencyProperty matrix3DProperty =
            DependencyProperty.Register("Matrix3D",
                typeof(Matrix3D), typeof(DisplayMatrix3D),
                new PropertyMetadata(Matrix3D.Identity, OnPropertyChanged));

        public DisplayMatrix3D()
        {
            this.InitializeComponent();
        }

        public static DependencyProperty Matrix3DProperty
        {
            get { return matrix3DProperty; }
        }

        public Matrix3D Matrix3D
        {
            set { SetValue(Matrix3DProperty, value); }
            get { return (Matrix3D)GetValue(Matrix3DProperty); }
        }

        static void OnPropertyChanged(DependencyObject obj,
                                      DependencyPropertyChangedEventArgs args)
        {
            (obj as DisplayMatrix3D).OnPropertyChanged(args);
        }

        private void OnPropertyChanged(DependencyPropertyChangedEventArgs args)
        {
            m11.Text = this.Matrix3D.M11.ToString("F3");
            m12.Text = this.Matrix3D.M12.ToString("F3");
            m13.Text = this.Matrix3D.M13.ToString("F3");
            m14.Text = this.Matrix3D.M14.ToString("F6");

            m21.Text = this.Matrix3D.M21.ToString("F3");
            m22.Text = this.Matrix3D.M22.ToString("F3");
            m23.Text = this.Matrix3D.M23.ToString("F3");
            m24.Text = this.Matrix3D.M24.ToString("F6");

            m31.Text = this.Matrix3D.M31.ToString("F3");
            m32.Text = this.Matrix3D.M32.ToString("F3");
            m33.Text = this.Matrix3D.M33.ToString("F3");
            m34.Text = this.Matrix3D.M34.ToString("F6");

            m41.Text = this.Matrix3D.OffsetX.ToString("F0");
            m42.Text = this.Matrix3D.OffsetY.ToString("F0");
            m43.Text = this.Matrix3D.OffsetZ.ToString("F0");
            m44.Text = this.Matrix3D.M44.ToString("F0");
        }
    }
}

Спецификации форматирования были выбраны на основе экспериментов с типичными диапазонами этих ячеек.

Файл XAML для MainPage наряду с экземпляром элемента управления DisplayMatrix3D содержит ссылку на изображение с моего веб-сайта, которое дополняется четырьмя элементами управления Thumb, при помощи которых можно перетащить любой угол в произвольное место. Префиксы «ul», «ur», «ll» и «lr» обозначают соответственно левый верхний, правый верхний, левый нижний и правый нижний угол.

<Page ...>

    <Page.Resources>
        <Style TargetType="Thumb">
            <Setter Property="Width" Value="48" />
            <Setter Property="Height" Value="48" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="VerticalAlignment" Value="Top" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <Image Source="http://professorweb.ru/my/windows8/rt/level1/files/win8logo.png"
               Stretch="None"
               HorizontalAlignment="Left"
               VerticalAlignment="Top">
            <Image.Projection>
                <Matrix3DProjection x:Name="matrixProjection" />
            </Image.Projection>
        </Image>

        <Thumb DragDelta="OnThumbDragDelta">
            <Thumb.RenderTransform>
                <TransformGroup>
                    <TranslateTransform X="-24" Y="-24" />
                    <TranslateTransform x:Name="ulTranslate" X="100" Y="100" />
                </TransformGroup>
            </Thumb.RenderTransform>
        </Thumb>

        <Thumb DragDelta="OnThumbDragDelta">
            <Thumb.RenderTransform>
                <TransformGroup>
                    <TranslateTransform X="-24" Y="-24" />
                    <TranslateTransform x:Name="urTranslate" X="420" Y="100" />
                </TransformGroup>
            </Thumb.RenderTransform>
        </Thumb>

        <Thumb DragDelta="OnThumbDragDelta">
            <Thumb.RenderTransform>
                <TransformGroup>
                    <TranslateTransform X="-24" Y="-24" />
                    <TranslateTransform x:Name="llTranslate" X="100" Y="500" />
                </TransformGroup>
            </Thumb.RenderTransform>
        </Thumb>

        <Thumb DragDelta="OnThumbDragDelta">
            <Thumb.RenderTransform>
                <TransformGroup>
                    <TranslateTransform X="-24" Y="-24" />
                    <TranslateTransform x:Name="lrTranslate" X="420" Y="500" />
                </TransformGroup>
            </Thumb.RenderTransform>
        </Thumb>

        <local:DisplayMatrix3D HorizontalAlignment="Right"
                               VerticalAlignment="Bottom"
                               FontSize="24"
                               Matrix3D="{Binding ElementName=matrixProjection,
                                                  Path=ProjectionMatrix}" />
    </Grid>
</Page>

Файл фонового кода реализует математику, приведенную ранее, не считая того, что для отображения фактического размера и позиции изображения в единичный квадрат требуется еще одно преобразование - матрица с именем S в коде CalculateNewTransform:

using Windows.Foundation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Media3D;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // Позиция и размер Image без преобразования
        Rect imageRect = new Rect(0, 0, 320, 400);

        public MainPage()
        {
            this.InitializeComponent();

            Loaded += (sender, args) =>
            {
                CalculateNewTransform();
            };
        }

        private void OnThumbDragDelta(object sender, DragDeltaEventArgs args)
        {
            Thumb thumb = sender as Thumb;
            TransformGroup xformGroup = thumb.RenderTransform as TransformGroup;
            TranslateTransform translate = xformGroup.Children[1] as TranslateTransform;
            translate.X += args.HorizontalChange;
            translate.Y += args.VerticalChange;
            CalculateNewTransform();
        }

        private void CalculateNewTransform()
        {
            Matrix3D matrix = CalculateNewTransform(imageRect,
                                    new Point(ulTranslate.X, ulTranslate.Y),
                                    new Point(urTranslate.X, urTranslate.Y),
                                    new Point(llTranslate.X, llTranslate.Y),
                                    new Point(lrTranslate.X, lrTranslate.Y));

            matrixProjection.ProjectionMatrix = matrix;
        }

        // Возвращает преобразование отображает точки (0, 0),
        //  (0, 1), (1, 0) и(1, 1) на точки
        //  ptUL, ptUR, ptLL и ptLR, нормализованные по rect.
        static Matrix3D CalculateNewTransform(Rect rect, Point ptUL, Point ptUR,
                                                         Point ptLL, Point ptLR)
        {
            // Нормализующее преобразование масштабирования и сдвига
            Matrix3D S = new Matrix3D()
            {
                M11 = 1 / rect.Width,
                M22 = 1 / rect.Height,
                OffsetX = -rect.Left / rect.Width,
                OffsetY = -rect.Top / rect.Height,
                M44 = 1
            };

            // Аффинное преобразование: отображает
            //      (0, 0) --> ptUL
            //      (1, 0) --> ptUR
            //      (0, 1) --> ptLL
            //      (1, 1) --> (x2 + x1 + x0, y2 + y1 + y0)
            Matrix3D A = new Matrix3D()
            {
                OffsetX = ptUL.X,
                OffsetY = ptUL.Y,
                M11 = (ptUR.X - ptUL.X),
                M12 = (ptUR.Y - ptUL.Y),
                M21 = (ptLL.X - ptUL.X),
                M22 = (ptLL.Y - ptUL.Y),
                M44 = 1
            };

            // Неафинное преобразование
            Matrix3D B = new Matrix3D();
            double den = A.M11 * A.M22 - A.M12 * A.M21;
            double a = (A.M22 * ptLR.X - A.M21 * ptLR.Y +
                                A.M21 * A.OffsetY - A.M22 * A.OffsetX) / den;
            double b = (A.M11 * ptLR.Y - A.M12 * ptLR.X +
                                A.M12 * A.OffsetX - A.M11 * A.OffsetY) / den;

            B.M11 = a / (a + b - 1);
            B.M22 = b / (a + b - 1);
            B.M14 = B.M11 - 1;
            B.M24 = B.M22 - 1;
            B.M44 = 1;

            // Произведение трех преобразований
            return S * B * A;
        }
    }
}

В отличие от двумерной структуры Matrix, структура Matrix3D реализует оператор умножения, существенно упрощающий манипуляции с массивами.

Конечно, один из ползунков можно перетащить в позицию, в которой изображение исчезнет, потому что по крайней мере один из углов окажется невыпуклым, и линии будут пересекать друг друга. Но при таких ограничениях изображение можно растянуть в неаффинную форму:

Редактирование трехмерного изображения

Конечно, чтобы заставить Windows Runtime применить неаффинное преобразование желаемой формы, придется немного потрудиться, но ваши усилия окупятся нелепым видом людей на деформированных фотографиях.

Пройди тесты
Лучший чат для C# программистов