Formeln

Thursday, March 21, 2013

Orbit Camera

Requirements

An Orbit Camera is a camera that orbits around a given point. We have to consider two angles: azimuth and pitch. Usually you rotate in a horizontal fashion (azimuth) and in vertical (pitch). When rotating around the poles of the resulting sphere the camera is moving on, it is not preferred to go over the poles. Therefore we lock the camera at the poles and prohibit walking over the poles.

Walking over a pole would result in two unwanted behaviours, depending on the implementation: if the up vector of the camera switches its sign when transgressing the pole, this would result in looking from upside-down at the  scene. If the up vector keeps its sign while transgressing a pole, the view rotates instantaneously by 180° around the vertical axis, when wandering over the pole, which is uncomfortable to watch.

Rotating around the vertical axis is accomplished by moving the mouse right and left. Moving the mouse up and down results in a motion of the camera around the horizontal axis. The mouse wheel is for zooming in and out.

Source Code for the Orbit Camera

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.DXGI;

namespace Apparat
{
    public class OrbitCamera
    {
        #region Singleton Pattern
        private static OrbitCamera instance = null;
        public static OrbitCamera Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new OrbitCamera();
                }
                return instance;
            }
        }
        #endregion

        #region Constructor
        private OrbitCamera()
        {
            eye = new Vector3(4, 2, 0);
            target = new Vector3(0, 0, 0);
            up = new Vector3(0, 1, 0);

            view = Matrix.LookAtLH(eye, target, up);
            perspective = Matrix.PerspectiveFovLH((float)Math.PI / 4, 1.3f, 0.0f, 1.0f);
        }
        #endregion

        Vector3 eye;
        Vector3 target;
        Vector3 up;

        Matrix view = Matrix.Identity;
        Matrix perspective = Matrix.Identity;
        Matrix viewPerspective = Matrix.Identity;

        public Matrix View
        {
            get { return view; }
        }

        public void setPerspective(float fov, float aspect, float znear, float zfar)
        {
            perspective = Matrix.PerspectiveFovLH(fov, aspect, znear, zfar);
        }

        public void setView(Vector3 eye, Vector3 target, Vector3 up)
        {
            view = Matrix.LookAtLH(eye, target, up);
        }

        public Matrix Perspective
        {
            get { return perspective; }
        }

        public Matrix ViewPerspective
        {
            get { return view * perspective; }
        }

        float rotY = 0;

        public void rotateY(int value)
        {
            rotY = (value / 100.0f);
            Matrix rotMat = Matrix.RotationY(rotY);
            eye = Vector3.TransformCoordinate(eye, rotMat);
            setView(eye, target, up);
        }
        float rotOrtho = 0;

        public void rotateOrtho(int value)
        {
            Vector3 viewDir = target - eye;
            Vector3 orhto = Vector3.Cross(viewDir, up);

            rotOrtho = (value / 100.0f);
            Matrix rotOrthoMat = Matrix.RotationAxis(orhto, rotOrtho);

            Vector3 eyeLocal = eye - target;
            eyeLocal = Vector3.TransformCoordinate(eyeLocal, rotOrthoMat);
            Vector3 newEye = eyeLocal + target;
            Vector3 newViewDir = target - newEye;
            float cosAngle = Vector3.Dot(newViewDir, up) / (newViewDir.Length() * up.Length());
            if (cosAngle < 0.9f && cosAngle > -0.9f)
            {
                eye = eyeLocal + target;
                setView(eye, target, up);
            }
        }


        float maxZoom = 3.0f;
        public void zoom(int value)
        {
            float scaleFactor = 1.0f;
            if (value > 0)
            {
                scaleFactor = 1.1f;
            }
            else
            {
                if ((eye - target).Length() > maxZoom)
                    scaleFactor = 0.9f;
            }

            Matrix scale = Matrix.Scaling(scaleFactor, scaleFactor, scaleFactor);
            eye = Vector3.TransformCoordinate(eye, scale);
            setView(eye, target, up);
        }
    }
}


The pose of the camera is defined by three vectors: up, eye and target. Up is a direction vector, that defines the up direction. Eye is the position of the camera and target is the position to look at. These vectors are set in the constructor of this class and are needed to create the look-at matrix, which we call view.

Here is the reference to the look at matrix:
http://slimdx.org/docs/html/M_SlimDX_Matrix_LookAtLH.htm
Every time, we change one of the three vectors up, eye or target, we call the setView method, in which we create a new view matrix.

Next we create the perspective matrix.
Reference: http://slimdx.org/docs/html/M_SlimDX_Matrix_PerspectiveFovLH.htm

The method rotateY is called, when moving the mouse left or right and preform a rotation around the y-axis.

The method rotateOrtho is called, when moving the mouse up or down. This method is named rotateOrtho, because the axis of rotation is orthogonal to up vector and the direction vector from the eye to the target. Here we also prevent the camera to transit the poles.

The zoom method is called, when using the mouse wheel.

Adapt the RenderControl

In order to control the camera with the mouse, we need to interact with the RenderControl. In order to do so, I handle four events:
  • MouseUp
  • MouseDown
  • MouseMove
  • MouseWheel

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Apparat.Renderables;

namespace Apparat
{
    public partial class RenderControl : UserControl
    {
        public RenderControl()
        {
            InitializeComponent();
            this.MouseWheel += new MouseEventHandler(RenderControl_MouseWheel);
        }

        public void init()
        {
            DeviceManager.Instance.createDeviceAndSwapChain(this);
            RenderManager.Instance.init();

            Grid grid = new Grid(10, 1.0f);
            TriangleEF triangle = new TriangleEF();
            Scene.Instance.addRenderObject(triangle);
            Scene.Instance.addRenderObject(grid);
        }

        public void shutDown()
        {
            RenderManager.Instance.shutDown();
            DeviceManager.Instance.shutDown();
        }

        public bool dragging = false;
        int startX = 0;
        int deltaX = 0;

        int startY = 0;
        int deltaY = 0;

        private void RenderControl_MouseUp(object sender, MouseEventArgs e)
        {
            dragging = false;
        }

        private void RenderControl_MouseDown(object sender, MouseEventArgs e)
        {
            dragging = true;
            startX = e.X;
            startY = e.Y;
        }

        private void RenderControl_MouseMove(object sender, MouseEventArgs e)
        {
            if (dragging)
            {
                int currentX = e.X;
                deltaX = startX - currentX;
                startX = currentX;

                int currentY = e.Y;
                deltaY = startY - currentY;
                startY = currentY;

                if (e.Button == System.Windows.Forms.MouseButtons.Left)
                {
                    OrbitCamera.Instance.rotateY(-deltaX);
                    OrbitCamera.Instance.rotateOrtho(deltaY);
                }
            }
        }

        void RenderControl_MouseWheel(object sender, MouseEventArgs e)
        {
            int delta = e.Delta;
            OrbitCamera.Instance.zoom(delta);
        }
    }
}

Adapt Renderables

The camera has a method called ViewPerspective, which return the result of a multiplication of the cameras view matrix with its perspective matrix. To set the according transformation in the renderables, this method has to be called in the render method of the renderables, e.g. the grid renderable:

public override void render()
{
  Matrix ViewPerspective = OrbitCamera.Instance.ViewPerspective;
  tmat.SetMatrix(ViewPerspective);

  // configure the Input Assembler portion of the pipeline with the vertex data
  DeviceManager.Instance.context.InputAssembler.InputLayout = layout;
  DeviceManager.Instance.context.InputAssembler.PrimitiveTopology = PrimitiveTopology.LineList;
  DeviceManager.Instance.context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBuffer, 12, 0));

  technique = effect.GetTechniqueByName("Render");

  EffectTechniqueDescription techDesc;
  techDesc = technique.Description;

  for (int p = 0; p < techDesc.PassCount; ++p)
  {
    technique.GetPassByIndex(p).Apply(DeviceManager.Instance.context);
    DeviceManager.Instance.context.Draw(numVertices, 0);
  }
}

Here we get the ViewPerspective matrix from the camera and pass it to the Effect variable. The results can be seen in the videos below.

Results

This video shows how the camera rotates around the center of the global coordinate system. Because the transformations of the triangle were not adjusted to the transformation from the camera, the triangle still rotates in the middle of the window.

This video was made, after the transformation of the triangle was adapted. Now the triangle is stationary. Because the triangle is culled just from one side, it is invisible, if the camera is looking at it from the other side.

You can download the source code to this tutorial here.

No comments:

Post a Comment