English 中文(简体)
WPF 3D - How can I save and load a Camera view?
原标题:

I have a WPF 3D scene where I can pan, rotate and zoom using the TrackballDecorator from the 3DTools library. I would like to save the camera settings (transformation) and be able to re-apply them when the application restarts the next time (so the view is restored).

I tried to save each individual value of the Camera:

private void SaveCameraSettings()
{
  var d = Properties.Settings.Default;
  d.CameraPositionX = camera.Position.X;
  d.CameraPositionY = camera.Position.Y;
  ...
  d.Save();
}

This doesn t work, I guess because those settings are not updated according to the transformations applied to the camera (I always get the initial values set in xaml).

I checked the the Transformation3D class but couldn t find any way to set its value...

The problem is what values do I need to get from the PerspectiveCamera in order to be able to restore it the way it was when I closed my application the last time. The camera is set to a default position (in Xaml), then a transformation is applied to this camera by the TrackBallDecorator. How can I save this transformation (what values to store)? And how can I re-apply them at a later time?

最佳回答

This is going to be a bit long, so bear with me...

1st, you need to modify the 3DTools library so you can apply a transformation to the TrackballDecorator as follow:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Input;

namespace _3DTools
{
  public class TrackballDecorator : Viewport3DDecorator
  {

    #region Private Members

    private Point m_PreviousPosition2D;
    private Vector3D m_PreviousPosition3D = new Vector3D(0, 0, 1);

    private Transform3DGroup m_Transform;
    private ScaleTransform3D m_Scale = new ScaleTransform3D();
    private AxisAngleRotation3D m_Rotation = new AxisAngleRotation3D();
    private TranslateTransform3D m_Translate = new TranslateTransform3D();

    private readonly Border m_EventSource;

    #endregion

    #region Constructor

    public TrackballDecorator()
    {
      TranslateScale = 10;
      ZoomScale = 1;
      RotateScale = 1;
      // the transform that will be applied to the viewport 3d s camera
      m_Transform = new Transform3DGroup();
      m_Transform.Children.Add(m_Scale);
      m_Transform.Children.Add(new RotateTransform3D(m_Rotation));
      m_Transform.Children.Add(m_Translate);

      // used so that we always get events while activity occurs within
      // the viewport3D
      m_EventSource = new Border { Background = Brushes.Transparent };

      PreViewportChildren.Add(m_EventSource);
    }

    #endregion

    #region Properties

    /// <summary>
    /// A transform to move the camera or scene to the trackball s
    /// current orientation and scale.
    /// </summary>
    public Transform3DGroup Transform
    {
      get { return m_Transform; }
      set
      {
        m_Transform = value;
        m_Scale = m_Transform.GetScaleTransform3D();
        m_Translate = m_Transform.GetTranslateTransform3D();
        m_Rotation = m_Transform.GetRotateTransform3D().Rotation as AxisAngleRotation3D;
        ApplyTransform();
      }
    }

    public double TranslateScale { get; set; }

    public double RotateScale { get; set; }

    public double ZoomScale { get; set; }

    #endregion

    #region Event Handling

    protected override void OnMouseDown(MouseButtonEventArgs e)
    {
      base.OnMouseDown(e);

      m_PreviousPosition2D = e.GetPosition(this);
      m_PreviousPosition3D = ProjectToTrackball(ActualWidth,
                                               ActualHeight,
                                               m_PreviousPosition2D);
      if (Mouse.Captured == null)
      {
        Mouse.Capture(this, CaptureMode.Element);
      }
    }

    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
      base.OnMouseUp(e);

      if (IsMouseCaptured)
      {
        Mouse.Capture(this, CaptureMode.None);
      }
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
      base.OnMouseMove(e);

      if (IsMouseCaptured)
      {
        Point currentPosition = e.GetPosition(this);

        // avoid any zero axis conditions
        if (currentPosition == m_PreviousPosition2D) return;

        // Prefer tracking to zooming if both buttons are pressed.
        if (e.LeftButton == MouseButtonState.Pressed)
        {
          Track(currentPosition);
        }
        else if (e.RightButton == MouseButtonState.Pressed)
        {
          Zoom(currentPosition);
        }
        else if (e.MiddleButton == MouseButtonState.Pressed)
        {
          Translate(currentPosition);
        }

        m_PreviousPosition2D = currentPosition;

        ApplyTransform();
      }
    }

    private void ApplyTransform()
    {
      Viewport3D viewport3D = Viewport3D;
      if (viewport3D != null)
      {
        if (viewport3D.Camera != null)
        {
          if (viewport3D.Camera.IsFrozen)
          {
            viewport3D.Camera = viewport3D.Camera.Clone();
          }

          if (viewport3D.Camera.Transform != m_Transform)
          {
            viewport3D.Camera.Transform = m_Transform;
          }
        }
      }
    }

    #endregion Event Handling

    private void Track(Point currentPosition)
    {
      var currentPosition3D = ProjectToTrackball(ActualWidth, ActualHeight, currentPosition);

      var axis = Vector3D.CrossProduct(m_PreviousPosition3D, currentPosition3D);
      var angle = Vector3D.AngleBetween(m_PreviousPosition3D, currentPosition3D);

      // quaterion will throw if this happens - sometimes we can get 3D positions that
      // are very similar, so we avoid the throw by doing this check and just ignoring
      // the event 
      if (axis.Length == 0) return;

      var delta = new Quaternion(axis, -angle);

      // Get the current orientantion from the RotateTransform3D
      var r = m_Rotation;
      var q = new Quaternion(m_Rotation.Axis, m_Rotation.Angle);

      // Compose the delta with the previous orientation
      q *= delta;

      // Write the new orientation back to the Rotation3D
      m_Rotation.Axis = q.Axis;
      m_Rotation.Angle = q.Angle;

      m_PreviousPosition3D = currentPosition3D;
    }

    private static Vector3D ProjectToTrackball(double width, double height, Point point)
    {
      var x = point.X / (width / 2);    // Scale so bounds map to [0,0] - [2,2]
      var y = point.Y / (height / 2);

      x = x - 1;                           // Translate 0,0 to the center
      y = 1 - y;                           // Flip so +Y is up instead of down

      var z2 = 1 - x * x - y * y;       // z^2 = 1 - x^2 - y^2
      var z = z2 > 0 ? Math.Sqrt(z2) : 0;

      return new Vector3D(x, y, z);
    }

    private void Zoom(Point currentPosition)
    {
      var yDelta = currentPosition.Y - m_PreviousPosition2D.Y;

      var scale = Math.Exp(yDelta / 100) / ZoomScale;    // e^(yDelta/100) is fairly arbitrary.

      m_Scale.ScaleX *= scale;
      m_Scale.ScaleY *= scale;
      m_Scale.ScaleZ *= scale;
    }

    private void Translate(Point currentPosition)
    {
      // Calculate the panning vector from screen(the vector component of the Quaternion
      // the division of the X and Y components scales the vector to the mouse movement
      var qV = new Quaternion(((m_PreviousPosition2D.X - currentPosition.X) / TranslateScale),
      ((currentPosition.Y - m_PreviousPosition2D.Y) / TranslateScale), 0, 0);

      // Get the current orientantion from the RotateTransform3D
      var q = new Quaternion(m_Rotation.Axis, m_Rotation.Angle);
      var qC = q;
      qC.Conjugate();

      // Here we rotate our panning vector about the the rotaion axis of any current rotation transform
      // and then sum the new translation with any exisiting translation
      qV = q * qV * qC;
      m_Translate.OffsetX += qV.X;
      m_Translate.OffsetY += qV.Y;
      m_Translate.OffsetZ += qV.Z;
    }

  }

}

The GetXXXTransform3D methods are extension methods defined as follow:

public static ScaleTransform3D GetScaleTransform3D(this Transform3DGroup transform3DGroup)
{
  ScaleTransform3D scaleTransform3D = null;
  if (transform3DGroup != null)
  {
    foreach (var transform in transform3DGroup.Children)
    {
      scaleTransform3D = transform as ScaleTransform3D;
      if (scaleTransform3D != null) return scaleTransform3D;
    }
  }
  return scaleTransform3D;
}

public static RotateTransform3D GetRotateTransform3D(this Transform3DGroup transform3DGroup)
{
  RotateTransform3D rotateTransform3D = null;
  if (transform3DGroup != null)
  {
    foreach (var transform in transform3DGroup.Children)
    {
      rotateTransform3D = transform as RotateTransform3D;
      if (rotateTransform3D != null) return rotateTransform3D;
    }
  }
  return rotateTransform3D;
}

public static TranslateTransform3D GetTranslateTransform3D(this Transform3DGroup transform3DGroup)
{
  TranslateTransform3D translateTransform3D = null;
  if (transform3DGroup != null)
  {
    foreach (var transform in transform3DGroup.Children)
    {
      translateTransform3D = transform as TranslateTransform3D;
      if (translateTransform3D != null) return translateTransform3D;
    }
  }
  return translateTransform3D;
}

2nd, you need to declare a Transform to your PerspectiveCamera as follow:
(the example is taken from Sasha Barber s Elements3D project which I used to test this)

<Tools:TrackballDecorator x:Name="tbViewPort">

  <Viewport3D x:Name="vpFeeds">

    <Viewport3D.Camera>
      <PerspectiveCamera x:Name="camera" Position="-2,2,40" LookDirection="2,-2,-40" FieldOfView="90">
        <PerspectiveCamera.Transform>
          <Transform3DGroup />
        </PerspectiveCamera.Transform>
      </PerspectiveCamera>
    </Viewport3D.Camera>

    <ContainerUIElement3D x:Name="container" />

    <ModelVisual3D x:Name="model">
      <ModelVisual3D.Content>
        <DirectionalLight Color="White" Direction="-1,-1,-1" />
      </ModelVisual3D.Content>
    </ModelVisual3D>

  </Viewport3D>
</Tools:TrackballDecorator>

3rd, since we are going to store each part of the whole transformation in a separate value, you need to create the relevant properties in your settings file, i.e. CameraScaleX, CameraScaleY, CameraScaleZ, CameraTranslateX, CameraTranslateY, CameraTranslateZ, CameraRotateAxisX, CameraRotateAxisY, CameraRotateAxisZ and CameraRotateAngle. All are of type double and are stored in User scope.

4th and last step is to actually save and load these settings into the camera using the following code:

private void SaveCameraSettings()
{
  var transform3DGroup = camera.Transform as Transform3DGroup;
  if (transform3DGroup != null)
  {
    foreach (var transform in transform3DGroup.Children)
    {
      var scale = transform as ScaleTransform3D;
      if (scale != null) SaveCameraSetting(scale);
      var rotate = transform as RotateTransform3D;
      if (rotate != null) SaveCameraSetting(rotate);
      var translate = transform as TranslateTransform3D;
      if (translate != null) SaveCameraSetting(translate);
    }
    Settings.Default.Save();
  }
}

private static void SaveCameraSetting(ScaleTransform3D transform)
{
  Properties.Settings.Default.CameraScaleX = transform.ScaleX;
  Properties.Settings.Default.CameraScaleY = transform.ScaleY;
  Properties.Settings.Default.CameraScaleZ = transform.ScaleZ;
}

private static void SaveCameraSetting(RotateTransform3D transform)
{
  var axisAngleRotation3D = transform.Rotation as AxisAngleRotation3D;
  if (axisAngleRotation3D != null)
  {
    Properties.Settings.Default.CameraRotateAxisX = axisAngleRotation3D.Axis.X;
    Properties.Settings.Default.CameraRotateAxisY = axisAngleRotation3D.Axis.Y;
    Properties.Settings.Default.CameraRotateAxisZ = axisAngleRotation3D.Axis.Z;
    Properties.Settings.Default.CameraRotateAngle = axisAngleRotation3D.Angle;
  }
}

private static void SaveCameraSetting(TranslateTransform3D transform)
{
  Properties.Settings.Default.CameraTranslateX = transform.OffsetX;
  Properties.Settings.Default.CameraTranslateY = transform.OffsetY;
  Properties.Settings.Default.CameraTranslateZ = transform.OffsetZ;
}

private void LoadCameraPosition()
{
  var d = Settings.Default;

  var transform3DGroup = new Transform3DGroup();

  var scaleTransform3D = new ScaleTransform3D(d.CameraScaleX, d.CameraScaleY, d.CameraScaleZ);
  var translateTransform3D = new TranslateTransform3D(d.CameraTranslateX, d.CameraTranslateY, d.CameraTranslateZ);
  var axisAngleRotation3D = new AxisAngleRotation3D(new Vector3D(d.CameraRotateAxisX, d.CameraRotateAxisY, d.CameraRotateAxisZ),
                                                    d.CameraRotateAngle);
  var rotateTransform3D = new RotateTransform3D(axisAngleRotation3D);

  transform3DGroup.Children.Add(scaleTransform3D);
  transform3DGroup.Children.Add(translateTransform3D);
  transform3DGroup.Children.Add(rotateTransform3D);

  tbViewPort.Transform = transform3DGroup;
}

Hopefully, I didn t forget anything. If you need more help or don t understand something, please don t hesitate to ask ;-)

问题回答

You would need both the cameras view matrix data and the projection matrix data. The view matrix will contain the data about the position, rotation, scale and translation of the camera and the projection matrix will contain things like the field of view, near plane, far plane and other data.

Sorry I cant help with exporting/importing that data since I ve not used WPF, but there might be rawdata properties exposed if it uses anything to do with as3 s built in Matrix classes, this is usualy a as3 Vector. Object the the matrices 16 values exposed as row ordered floating point values.

I believe what you need is Position, LookDirection, UpDirection, FieldOfView, NearPlaneDistance, FarPlaneDistance. All the above properties define the camera.





相关问题
WPF convert 2d mouse click into 3d space

I have several geometry meshes in my Viewport3D, these have bounds of (w:1800, h:500, d:25). When a user clicks in the middle of the mesh, I want the Point3D of (900, 500, 25)... How can I achieve ...

Editing a xaml icons or images

Is it possible to edit a xaml icons or images in the expression design or using other tools? Is it possible to import a xaml images (that e.g you have exported) in the expression designer for editing?...

WPF: writing smoke tests using ViewModels

I am considering to write smoke tests for our WPF application. The question that I am faced is: should we use UI automation( or some other technology that creates a UI script), or is it good enough to ...

WPF - MVVM - NHibernate Validation

Im facing a bit of an issue when trying to validate a decimal property on domain object which is bound to a textbox on the view through the viewmodel. I am using NHibernate to decorate my property on ...

How do WPF Markup Extensions raise compile errors?

Certain markup extensions raise compile errors. For example StaticExtension (x:Static) raises a compile error if the referenced class cannot be found. Anyone know the mechanism for this? Is it baked ...

WPF design-time context menu

I am trying to create a custom wpf control, I m wondering how I can add some design-time features. I ve googled and can t seem to get to my goal. So here s my simple question, how can I add an entry ...

How to combine DataTrigger and EventTrigger?

NOTE I have asked the related question (with an accepted answer): How to combine DataTrigger and Trigger? I think I need to combine an EventTrigger and a DataTrigger to achieve what I m after: when ...

热门标签