Ориентация в трехмерном пространстве в WinRT

80

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

SensorQuaternion
SensorRotationMatrix

Кватернионы (quaternion) весьма интересны с математической точки зрения. Подобно тому, как комплексное число может представлять поворот в двумерном пространстве, кватернион представляет поворот в трехмерном пространстве. Специалисты по программированию игр особенно любят представлять повороты кватернионами, потому что кватернионы поддерживают гладкую интерполяцию.

Матрица поворота представляет собой обычную матрицу преобразования без последней строки и последнего столбца. Обычная матрица трехмерного преобразования состоит из 4 строк и 4 столбцов. Класс SensorRotationMatrix определяет 3 строки и 3 столбца. Такая матрица не способна представлять смещение или перспективу, и по правилам в нее не включается ни масштабирование, ни деформация. При этом она легко может использоваться для вращения объектов в трехмерном пространстве.

На планшете Samsung матрица SensorRotationMatrix заполняется нулями, так что ни одна программа, использующая эту матрицу, на нем работать не будет. На Microsoft Surface поддержка OrientationSensor работает лучше.

При работе с матрицами поворота иногда бывает полезно сменить «точку зрения». Я уже описывал, какое место данные Accelerometer и Compass занимают в трехмерной системе координат. При работе с матрицами поворота из класса OrientationSensor полезно представить две разные системы координат — для устройства и для Земли:

Различия продемонстрированы в программе AxisAngleRotation, которая вычисляет еще одну форму представления поворотов в трехмерном пространстве — поворота относительно трехмерного вектора. Файл XAML содержит не слишком интересный набор элементов TextBlock; одни из них используются в качестве меток, другие ожидают получения текста:

<Page ...>

    <Page.Resources>
        <Style x:Key="DefaultTextBlockStyle" TargetType="TextBlock">
            <Setter Property="FontFamily" Value="Lucida Sans Unicode" />
            <Setter Property="FontSize" Value="36" />
            <Setter Property="Margin" Value="0 0 48 0" />
        </Style>

        <Style x:Key="rightText" TargetType="TextBlock" 
               BasedOn="{StaticResource DefaultTextBlockStyle}">
            <Setter Property="TextAlignment" Value="Right" />
            <Setter Property="Margin" Value="48 0 0 0" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center">

            <!-- Панель Grid с углами тангажа, крена и отклонения -->
            <Grid HorizontalAlignment="Center">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

                <Grid.Resources>
                    <Style TargetType="TextBlock"
                           BasedOn="{StaticResource DefaultTextBlockStyle}"></Style>
                </Grid.Resources>

                <TextBlock Text="Pitch: " Grid.Row="0" Grid.Column="0" />
                <TextBlock Name="pitchText" Grid.Row="0" Grid.Column="1" 
                           Style="{StaticResource rightText}" />
                
                <TextBlock Text="Roll: " Grid.Row="1" Grid.Column="0" />
                <TextBlock Name="rollText" Grid.Row="1" Grid.Column="1" 
                           Style="{StaticResource rightText}" />
                
                <TextBlock Text="Yaw: " Grid.Row="2" Grid.Column="0" />
                <TextBlock Name="yawText" Grid.Row="2" Grid.Column="1" 
                           Style="{StaticResource rightText}" />
            </Grid>

            <!-- Панель Grid для RotationMatrix -->
            <Grid HorizontalAlignment="Center"
                  Margin="0 48">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

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

                <Grid.Resources>
                    <Style TargetType="TextBlock" 
                           BasedOn="{StaticResource rightText}"></Style>
                </Grid.Resources>

                <TextBlock Name="m11Text" Grid.Row="0" Grid.Column="0" />
                <TextBlock Name="m12Text" Grid.Row="0" Grid.Column="1" />
                <TextBlock Name="m13Text" Grid.Row="0" Grid.Column="2" />

                <TextBlock Name="m21Text" Grid.Row="1" Grid.Column="0" />
                <TextBlock Name="m22Text" Grid.Row="1" Grid.Column="1" />
                <TextBlock Name="m23Text" Grid.Row="1" Grid.Column="2" />

                <TextBlock Name="m31Text" Grid.Row="2" Grid.Column="0" />
                <TextBlock Name="m32Text" Grid.Row="2" Grid.Column="1" />
                <TextBlock Name="m33Text" Grid.Row="2" Grid.Column="2" />
            </Grid>

            <!-- Вывод оси/угла поворота -->
            <Grid HorizontalAlignment="Center">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

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

                <Grid.Resources>
                    <Style TargetType="TextBlock" BasedOn="{StaticResource DefaultTextBlockStyle}"></Style>
                </Grid.Resources>

                <TextBlock Text="Angle:" Grid.Row="0" Grid.Column="0" />
                <TextBlock Name="angleText" Grid.Row="0" Grid.Column="1" TextAlignment="Center"/>
                <TextBlock Text="Axis:" Grid.Row="1" Grid.Column="0" />
                <TextBlock Name="axisText" Grid.Row="1" Grid.Column="1" TextAlignment="Center" />
            </Grid>
        </StackPanel>
    </Grid>
</Page>

В файле фонового кода создаются экземпляры как Inclinometer для получения углов тангажа, крена и отклонения, так и OrientationSensor для получения (и вывода) матрицы поворота и ее преобразования к данным оси/угла поворота:

using System;
using Windows.Devices.Sensors;
using Windows.Graphics.Display;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        Inclinometer inclinometer = Inclinometer.GetDefault();
        OrientationSensor orientationSensor = OrientationSensor.GetDefault();

        public MainPage()
        {
            this.InitializeComponent();
            DisplayProperties.AutoRotationPreferences = DisplayProperties.NativeOrientation;
            Loaded += OnMainPageLoaded;
        }

        private async void OnMainPageLoaded(object sender, RoutedEventArgs e)
        {
            if (inclinometer == null)
            {
                await new MessageDialog("Датчик угла поворота не поддерживается").ShowAsync();    
            }
            else
            {
                // Запуск событий Inclinometer
                ShowYawPitchRoll(inclinometer.GetCurrentReading());
                inclinometer.ReadingChanged += OnInclinometerReadingChanged;
            }

            if (orientationSensor == null)
            {
                await new MessageDialog("Датчик положения в трехмерном пространстве не поддерживается").ShowAsync();
            }
            else
            {
                // Запуск событий OrientationSensor
                ShowOrientation(orientationSensor.GetCurrentReading());
                orientationSensor.ReadingChanged += OrientationSensorChanged;
            }
        }

        private async void OnInclinometerReadingChanged(Inclinometer sender, 
            InclinometerReadingChangedEventArgs e)
        {
            await this.Dispatcher.RunAsync(
                CoreDispatcherPriority.Normal, () =>
                {
                    ShowYawPitchRoll(e.Reading);
                });
        }

        private void ShowYawPitchRoll(InclinometerReading inclinometerReading)
        {
            if (inclinometerReading == null)
                return;

            yawText.Text = inclinometerReading.YawDegrees.ToString("F0") + "°";
            pitchText.Text = inclinometerReading.PitchDegrees.ToString("F0") + "°";
            rollText.Text = inclinometerReading.RollDegrees.ToString("F0") + "°";
        }

        private async void OrientationSensorChanged(OrientationSensor sender, 
            OrientationSensorReadingChangedEventArgs e)
        {
            await this.Dispatcher.RunAsync(
                CoreDispatcherPriority.Normal, () =>
                {
                    ShowOrientation(e.Reading);
                });
        }

        private void ShowOrientation(OrientationSensorReading orientationReading)
        {
            if (orientationReading == null)
                return;

            SensorRotationMatrix matrix = orientationReading.RotationMatrix;

            if (matrix == null)
                return;

            m11Text.Text = matrix.M11.ToString("F3");
            m12Text.Text = matrix.M12.ToString("F3");
            m13Text.Text = matrix.M13.ToString("F3");

            m21Text.Text = matrix.M21.ToString("F3");
            m22Text.Text = matrix.M22.ToString("F3");
            m23Text.Text = matrix.M23.ToString("F3");

            m31Text.Text = matrix.M31.ToString("F3");
            m32Text.Text = matrix.M32.ToString("F3");
            m33Text.Text = matrix.M33.ToString("F3");

            // Преобразование матрицы поворота в ось и угол
            double angle = Math.Acos((matrix.M11 + matrix.M22 + matrix.M33 - 1) / 2);
            angleText.Text = (180 * angle / Math.PI).ToString("F0");

            if (angle != 0)
            {
                double twoSine = 2 * Math.Sin(angle);
                double x = (matrix.M23 - matrix.M32) / twoSine;
                double y = (matrix.M31 - matrix.M13) / twoSine;
                double z = (matrix.M12 - matrix.M21) / twoSine;

                axisText.Text = String.Format("({0:F2} {1:F2} {2:F2})", x, y, z);
            }
        }
    }
}

На следующем снимке экрана с Microsoft Surface сверху выводятся три «авиационных» угла, в середине — матрица поворота, а внизу — вычисленная ось/угол поворота:

Пример использования датчика ориентации в трехмерном пространстве

Делая этот снимок, я направил планшет приблизительно на север, чтобы отклонение оставалось в окрестности нуля. Планшет был слегка наклонен влево с незначительным отрицательным креном. При этом верхняя сторона планшета была поднята на целых 46°; этот же угол отображается внизу как вычисленный по матрице поворота. Но взгляните на ось: она очень близка к вектору (-1,0,0), то есть отрицательной оси X. Используя правило правой руки, направьте большой палец в сторону отрицательной оси X. Изгиб пальцев определяет направление поворота для положительных углов, что подтверждает сказанное ранее: матрица поворота описывает поворот Земли относительно компьютера. Следовательно, для получения матрицы поворота, описывающей поворот компьютера относительно Земли, матрицу необходимо инвертировать.

Класс SensorRotationMatrix не обладает средствами инвертирования, но такие средства предусмотрены в структуре Matrix3D (напомню, что Matrix3D определяется в пространстве имен Windows.UI.Xaml.Media и используется с Matrix3DProjection). Остается создать значение Matrix3D по объекту SensorRotationMatrix и инвертировать его. Я использую этот метод для создания еще одного представления ориентации в трехмерном пространстве.

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