Трехмерное вращение точки вокруг произвольной оси

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

Карта вращается, как и ожидалось, вокруг вектора (-5, 1, 1) в начале координат. У меня возникли проблемы с поворотом точки на плоскости XY в нужное положение в трех измерениях.

Вот что я делаю:

private Point3D MapXY2Screen3D
    (
    MapPoint mapPoint
    )
{
    // Width and Height of the map, both 600
    double Xmax = Map.ActualWidth;
    double Ymax = Map.ActualHeight;

    // Convert ESRI map coordinates to screen coordinates
    Point ScreenCoordinates = Map.MapToScreen(mapPoint);

    // Normalize the screen coordinates so they fall in the range -1..1
    ScreenCoordinates.X = ((2*ScreenCoordinates.X)/Xmax) - 1;
    ScreenCoordinates.Y = 1 - ((2*ScreenCoordinates.Y)/Ymax);

    // Create a Quaternion from the original location: Petzold 
    // Chapter 8 - Low-Level Quaternion Rotation
    var originQuaternion = 
        new Quaternion(ScreenCoordinates.X, ScreenCoordinates.Y, 0, 0);

    // Multiply rotation quaternion by origin by conjugate of rotation quaternion
    // to get the rotated point as a quaternion.
    var rotatedPoint = RotationQuaternion *
                       originQuaternion *
                       RotationQuaternionConjugate;

    // Return the X, Y, Z of the rotated quaternion as a 3D point
    return new Point3D(rotatedPoint.X, rotatedPoint.Y, rotatedPoint.Z);
}

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

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

Во-вторых, в примерах, которые я рассматриваю, код выглядит немного иначе:

    // Create a Quaternion from the original location: Petzold 
    // Chapter 8 - Low-Level Quaternion Rotation
    var originQuaternion = 
        new Quaternion(ScreenCoordinates.X, ScreenCoordinates.Y, 0, 0);

    *originQuaternion -= center;*

    // Multiply rotation quaternion by origin by conjugate of rotation quaternion
    // to get the rotated point as a quaternion.
    var rotatedPoint = RotationQuaternion *
                       originQuaternion *
                       RotationQuaternionConjugate;

    *rotatedPoint += center;*

center представляет собой кватернион, указывающий на центр вращения. Я вращаюсь вокруг начала координат, поэтому думаю, что мне не нужно этого делать.

Вот несколько изображений, демонстрирующих размещение:

В 2д. Интересная точка представляет собой серый квадрат в секции СВ. 2d изображение программы

В 3д. Вы все еще можете видеть точку на плоскости карты. 3D изображение программы

В 3д с точкой создаю. Ожидаемый красный круг на сером квадрате. введите здесь описание изображения

Полный код

MainWindow.xaml.cs

using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Media3D;
using ESRI.ArcGIS.Client;
using ESRI.ArcGIS.Client.Geometry;
using ESRI.ArcGIS.Client.Symbols;
using Petzold.Media3D;

namespace Map3D
{
    /// <summary>
    ///     Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : INotifyPropertyChanged
    {
        private Quaternion _rotationQuaternion;
        private Quaternion _rotationQuaternionConjugate;
        private MapPoint _testMapPoint;

        #region PropertyChanged

        // General property changed event
        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            SetQuaternions();
        }

        public Quaternion RotationQuaternion
        {
            get { return _rotationQuaternion; }
            set
            {
                _rotationQuaternion = value;
                RaisePropertyChanged("RotationQuaternion");
            }
        }

        public Quaternion RotationQuaternionConjugate
        {
            get { return _rotationQuaternionConjugate; }
            set
            {
                _rotationQuaternionConjugate = value;
                RaisePropertyChanged("RotationQuaternionConjugate");
            }
        }

        public MapPoint TestMapPoint
        {
            get { return _testMapPoint; }
            set
            {
                _testMapPoint = value;
                RaisePropertyChanged("TestMapPoint");
            }
        }

        private void SetQuaternions()
        {
            var rotationAxis = new Vector3D(-5, 1, 1);
            rotationAxis.Normalize();

            RotationQuaternion = new Quaternion(rotationAxis, 65);
            RotationQuaternionConjugate = RotationQuaternion;
            RotationQuaternionConjugate.Conjugate();

            if (!RotationQuaternion.IsNormalized)
            {
                RotationQuaternion.Normalize();
            }
            if (!RotationQuaternionConjugate.IsNormalized)
            {
                RotationQuaternionConjugate.Normalize();
            }
        }

        private void Timeline_OnCompleted
            (
            object sender,
            EventArgs e
            )
        {
            Axes axes = new Axes();
            axes.Color = Colors.Red;
            axes.Extent = 1;
            axes.SetValue(Panel.ZIndexProperty, 100);
            MyModelVisual3D.Children.Add(axes);
        }

        private void Layer_OnInitialized
            (
            object sender,
            EventArgs e
            )
        {
            var graphic = new Graphic();
            var symbol = new SimpleMarkerSymbol
            {
                Style = SimpleMarkerSymbol.SimpleMarkerStyle.Circle,
                Size = 10,
                Color = new SolidColorBrush(Colors.DarkGray)
            };
            graphic.Symbol = symbol;

            var center = Map.Extent.GetCenter().Clone();

            MapPoint mapPoint = new MapPoint
            {
                X = 3*(Map.Extent.Width / 4) + Map.Extent.XMin,
                Y = 3*(Map.Extent.Height / 4) + Map.Extent.YMin
            };

            TestMapPoint = mapPoint;

            graphic.Geometry = mapPoint;

            var graphicsLayer = new GraphicsLayer
            {
                ID = "GraphicsLayer"
            };
            graphicsLayer.Graphics.Add(graphic);

            Map.Layers.Add(graphicsLayer);
        }

        private void PointButtonClick
            (
            object sender,
            RoutedEventArgs e
            )
        {
            GraphicsLayer graphicsLayer = 
                (GraphicsLayer) Map.Layers["GraphicsLayer"];
            MapPoint mapPoint = 
                (MapPoint) graphicsLayer.Graphics.First().Geometry;

            MyModelVisual3D.Children.Add(
                CreateSphere(0.025, Colors.Red, MapXY2Screen3D(mapPoint)));
        }

        private Visual3D CreateSphere
            (
            double radius,
            Color color,
            Point3D pt
            )
        {
            return new Sphere
            {
                Radius = radius,
                BackMaterial = new DiffuseMaterial
                {
                    Brush = new SolidColorBrush(color)
                },
                Center = pt,
            };
        }

        private Point3D MapXY2Screen3D
            (
            MapPoint mapPoint
            )
        {
            // Width and Height of the map, both 600
            double Xmax = Map.ActualWidth;
            double Ymax = Map.ActualHeight;

            // Convert ESRI map coordinates to screen coordinates
            Point ScreenCoordinates = Map.MapToScreen(mapPoint);

            // Normalize the screen coordinates so they fall in the range -1..1
            ScreenCoordinates.X = ((2*ScreenCoordinates.X)/Xmax) - 1;
            ScreenCoordinates.Y = 1 - ((2*ScreenCoordinates.Y)/Ymax);

            // Create a Quaternion from the original location: Petzold 
            // Chapter 8 - Low-Level Quaternion Rotation
            var originQuaternion = new Quaternion(
                ScreenCoordinates.X, ScreenCoordinates.Y, 0, 0);

            // Multiply rotation quaternion by origin by conjugate of rotation 
            // quaternion to get the rotated point as a quaternion.
            var rotatedPoint = RotationQuaternion *
                               originQuaternion *
                               RotationQuaternionConjugate;

            // Return the X, Y, Z of the rotated quaternion as a 3D point
            return new Point3D(rotatedPoint.X, rotatedPoint.Y, rotatedPoint.Z);
        }

        private void TiltButtonClick
            (
            object sender,
            RoutedEventArgs e
            )
        {
            var sb = Resources["MyStoryboard"] as Storyboard;
            sb.Begin();
        }
    }
}

MainWindow.xaml

<Window x:Class="Map3D.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
    Title="MainWindow"
    Width="300" Height="300" mc:Ignorable="d" SizeToContent="WidthAndHeight">
    <Window.Resources>
        <Storyboard x:Key="MyStoryboard">
            <QuaternionAnimation Storyboard.TargetName="MyQuaternionRotation3D"
                                 Storyboard.TargetProperty="Quaternion"
                                 To="{Binding RotationQuaternion}"
                                 Completed="Timeline_OnCompleted"
                                 Duration="0:0:1" />
        </Storyboard>
    </Window.Resources>
    <Grid Width="800" Height="800">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Viewport3D Grid.Row="0">
            <Viewport3D.Camera>
                <PerspectiveCamera Position="0,0,4"
                                   LookDirection="0,0,-1"
                                   UpDirection="0,1,0" />
            </Viewport3D.Camera>
            <ModelVisual3D x:Name="MyModelVisual3D">
                <ModelVisual3D.Content>
                    <AmbientLight Color="White" />
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <Viewport2DVisual3D>
                    <Viewport2DVisual3D.Transform>
                        <RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
                            <RotateTransform3D.Rotation>
                                <QuaternionRotation3D x:Name="MyQuaternionRotation3D"
                                                      Quaternion="0,0,0,0.5" />
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                    </Viewport2DVisual3D.Transform>
                    <Viewport2DVisual3D.Geometry>
                        <MeshGeometry3D Positions="-1 1 0, -1 -1 0, 1 -1 0, 1 1 0 "
                                        TextureCoordinates="0 0, 0 1, 1 1, 1 0"
                                        TriangleIndices="0 1 2 0 2 3" />
                    </Viewport2DVisual3D.Geometry>
                    <Viewport2DVisual3D.Material>
                        <DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True" 
                                         Brush=White" />
                    </Viewport2DVisual3D.Material>

                    <!-- Map -->
                    <Grid x:Name="MapGrid" 
                          Width="600" Height="600" 
                          Panel.ZIndex="-1" Opacity="1" 
                          ClipToBounds="True">
                        <Border Background="Black">
                            <Border.Effect>
                                <BlurEffect Radius="15" />
                            </Border.Effect>
                        </Border>

                        <esri:Map x:Name="Map" Extent="5071751, 2615619, 6622505, 3609912">
                            <esri:Map.Layers>
                                <esri:ArcGISTiledMapServiceLayer
                                    Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"
                                    Initialized="Layer_OnInitialized"
                                    Visible="True" />
                            </esri:Map.Layers>
                        </esri:Map>
                    </Grid>
                </Viewport2DVisual3D>
            </ModelVisual3D>
        </Viewport3D>
        <UniformGrid Grid.Row="1"
                     Rows="1" Columns="2">
            <Button Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"     Click="PointButtonClick">Point</Button>
            <Button Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"     Click="TiltButtonClick">Tilt</Button>
        </UniformGrid>
    </Grid>
</Window>

person Max Hampton    schedule 12.10.2014    source источник


Ответы (1)


В коде, который я разместил выше, сопряжение кватерниона вращения не вычислялось или не сохранялось правильно, поэтому я, по сути, умножал:

rotated point = rotationQ * origin * rotationQ

Это дает отражение, а не вращение. Чтобы исправить это, я изменил свой метод SetQuaternions на:

private void SetQuaternions()
{
    var rotationAxis = new Vector3D(-5f, 1f, 1f);

    RotationQuaternion = new Quaternion(rotationAxis, 65f);
    RotationQuaternion.Normalize();

    var q = RotationQuaternion;
    q.Conjugate();
    RotationQuaternionConjugate = q;

    if (!RotationQuaternion.IsNormalized)
    {
        RotationQuaternion.Normalize();
    }
    if (!RotationQuaternionConjugate.IsNormalized)
    {
        RotationQuaternionConjugate.Normalize();
    }
}

С этим изменением мои объекты Sphere создаются на плоскости карты в точке интереса.

person Max Hampton    schedule 12.10.2014