Трехмерные матричные преобразования в 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# программистов