title: 3D Rotation of a Point Around an Arbitrary Axis
body: <p>For a project I am currently working on, I am trying to work with a 2 dimensional map in 3 dimensions. For this project, I will need to be able to work with points on the map (which are defined in two dimensions) in three dimensions.</p>
  
  <p>The map rotating as expected around the vector (-5, 1, 1) at the origin. I am having trouble rotating a point on the XY plane into the appropriate position in three dimensions.</p>
  
  <p>Here's what I'm doing:</p>
  
  <pre><code>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);
  }
  </code></pre>
  
  <p>I can see a couple of potential issues that may be throwing the location of the points I am creating off of where they should be. </p>
  
  <p>First, if the normalization of the map coordinates is being done incorrectly, the point will clearly be in the wrong position after rotation. To me, these formulas make sense, and from some simple debugging, the results look right.</p>
  
  <p>Second, in the examples I am looking at, the code looks a little bit different:</p>
  
  <pre><code>    // 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;*
  </code></pre>
  
  <p>center is a quaternion indicating the center of rotation. I am rotating around the origin, so I think that I don't need to be doing this.</p>
  
  <p>Here are some images demonstrating the placement:</p>
  
  <p>In 2d. The point of interest is a grey square in the NE section.
  <img src="https://i.stack.imgur.com/09uVk.png" alt="2d image of program"></p>
  
  <p>In 3d. You can still see the point on the map plane.
  <img src="https://i.stack.imgur.com/U8ZNc.png" alt="3d image of program"></p>
  
  <p>In 3d with point I am creating. Expected red circle on the grey square.
  <img src="https://i.stack.imgur.com/r6k31.png" alt="enter image description here"></p>
  
  <h1>Full Code</h1>
  
  <h2>MainWindow.xaml.cs</h2>
  
  <pre><code>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
  {
      /// &lt;summary&gt;
      ///     Interaction logic for MainWindow.xaml
      /// &lt;/summary&gt;
      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();
          }
      }
  }
  </code></pre>
  
  <h2>MainWindow.xaml</h2>
  
  <pre><code>&lt;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"&gt;
      &lt;Window.Resources&gt;
          &lt;Storyboard x:Key="MyStoryboard"&gt;
              &lt;QuaternionAnimation Storyboard.TargetName="MyQuaternionRotation3D"
                                   Storyboard.TargetProperty="Quaternion"
                                   To="{Binding RotationQuaternion}"
                                   Completed="Timeline_OnCompleted"
                                   Duration="0:0:1" /&gt;
          &lt;/Storyboard&gt;
      &lt;/Window.Resources&gt;
      &lt;Grid Width="800" Height="800"&gt;
          &lt;Grid.RowDefinitions&gt;
              &lt;RowDefinition Height="*" /&gt;
              &lt;RowDefinition Height="Auto" /&gt;
          &lt;/Grid.RowDefinitions&gt;
          &lt;Viewport3D Grid.Row="0"&gt;
              &lt;Viewport3D.Camera&gt;
                  &lt;PerspectiveCamera Position="0,0,4"
                                     LookDirection="0,0,-1"
                                     UpDirection="0,1,0" /&gt;
              &lt;/Viewport3D.Camera&gt;
              &lt;ModelVisual3D x:Name="MyModelVisual3D"&gt;
                  &lt;ModelVisual3D.Content&gt;
                      &lt;AmbientLight Color="White" /&gt;
                  &lt;/ModelVisual3D.Content&gt;
              &lt;/ModelVisual3D&gt;
              &lt;ModelVisual3D&gt;
                  &lt;Viewport2DVisual3D&gt;
                      &lt;Viewport2DVisual3D.Transform&gt;
                          &lt;RotateTransform3D CenterX="0" CenterY="0" CenterZ="0"&gt;
                              &lt;RotateTransform3D.Rotation&gt;
                                  &lt;QuaternionRotation3D x:Name="MyQuaternionRotation3D"
                                                        Quaternion="0,0,0,0.5" /&gt;
                              &lt;/RotateTransform3D.Rotation&gt;
                          &lt;/RotateTransform3D&gt;
                      &lt;/Viewport2DVisual3D.Transform&gt;
                      &lt;Viewport2DVisual3D.Geometry&gt;
                          &lt;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" /&gt;
                      &lt;/Viewport2DVisual3D.Geometry&gt;
                      &lt;Viewport2DVisual3D.Material&gt;
                          &lt;DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True" 
                                           Brush=White" /&gt;
                      &lt;/Viewport2DVisual3D.Material&gt;
  
                      &lt;!-- Map --&gt;
                      &lt;Grid x:Name="MapGrid" 
                            Width="600" Height="600" 
                            Panel.ZIndex="-1" Opacity="1" 
                            ClipToBounds="True"&gt;
                          &lt;Border Background="Black"&gt;
                              &lt;Border.Effect&gt;
                                  &lt;BlurEffect Radius="15" /&gt;
                              &lt;/Border.Effect&gt;
                          &lt;/Border&gt;
  
                          &lt;esri:Map x:Name="Map" Extent="5071751, 2615619, 6622505, 3609912"&gt;
                              &lt;esri:Map.Layers&gt;
                                  &lt;esri:ArcGISTiledMapServiceLayer
                                      Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"
                                      Initialized="Layer_OnInitialized"
                                      Visible="True" /&gt;
                              &lt;/esri:Map.Layers&gt;
                          &lt;/esri:Map&gt;
                      &lt;/Grid&gt;
                  &lt;/Viewport2DVisual3D&gt;
              &lt;/ModelVisual3D&gt;
          &lt;/Viewport3D&gt;
          &lt;UniformGrid Grid.Row="1"
                       Rows="1" Columns="2"&gt;
              &lt;Button Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"     Click="PointButtonClick"&gt;Point&lt;/Button&gt;
              &lt;Button Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"     Click="TiltButtonClick"&gt;Tilt&lt;/Button&gt;
          &lt;/UniformGrid&gt;
      &lt;/Grid&gt;
  &lt;/Window&gt;
  </code></pre>