Трехмерные матричные преобразования в WinRT
145WinRT --- Трехмерные матричные преобразования
А теперь мы займемся неприятным делом - математикой. Как вы уже знаете, двумерная графика требует использования матрицы преобразования 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 применить неаффинное преобразование желаемой формы, придется немного потрудиться, но ваши усилия окупятся нелепым видом людей на деформированных фотографиях.