【OpenGL(SharpGL)】支持任意相机可平移缩放的轨迹球

(本文PDF版在这里。)

在3D程序中,轨迹球(ArcBall)可以让你只用鼠标来控制模型(旋转),便于观察。在这里(http://www.yakergong.net/nehe/ )有nehe的轨迹球教程。

本文提供一个本人编写的轨迹球类(ArcBall.cs),它可以直接应用到任何camera下,还可以同时实现缩放平移。工程源代码在文末。

2016-07-08

再次更新了轨迹球代码,重命名为ArcBallManipulater。

     /// <summary>
/// Rotate model using arc-ball method.
/// </summary>
public class ArcBallManipulater : Manipulater, IMouseHandler
{ private ICamera camera;
private GLCanvas canvas; private MouseEventHandler mouseDownEvent;
private MouseEventHandler mouseMoveEvent;
private MouseEventHandler mouseUpEvent;
private MouseEventHandler mouseWheelEvent; private vec3 _vectorRight;
private vec3 _vectorUp;
private vec3 _vectorBack;
private float _length, _radiusRadius;
private CameraState cameraState = new CameraState();
private mat4 totalRotation = mat4.identity();
private vec3 _startPosition, _endPosition, _normalVector = new vec3(, , );
private int _width;
private int _height;
private bool mouseDownFlag; public float MouseSensitivity { get; set; } public MouseButtons BindingMouseButtons { get; set; }
private MouseButtons lastBindingMouseButtons; /// <summary>
/// Rotate model using arc-ball method.
/// </summary>
/// <param name="bindingMouseButtons"></param>
public ArcBallManipulater(MouseButtons bindingMouseButtons = MouseButtons.Left)
{
this.MouseSensitivity = 0.1f;
this.BindingMouseButtons = bindingMouseButtons; this.mouseDownEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseDown);
this.mouseMoveEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseMove);
this.mouseUpEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseUp);
this.mouseWheelEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseWheel);
} private void SetCamera(vec3 position, vec3 target, vec3 up)
{
_vectorBack = (position - target).normalize();
_vectorRight = up.cross(_vectorBack).normalize();
_vectorUp = _vectorBack.cross(_vectorRight).normalize(); this.cameraState.position = position;
this.cameraState.target = target;
this.cameraState.up = up;
} class CameraState
{
public vec3 position;
public vec3 target;
public vec3 up; public bool IsSameState(ICamera camera)
{
if (camera.Position != this.position) { return false; }
if (camera.Target != this.target) { return false; }
if (camera.UpVector != this.up) { return false; } return true;
}
} public mat4 GetRotationMatrix()
{
return totalRotation;
} public override void Bind(ICamera camera, GLCanvas canvas)
{
if (camera == null || canvas == null) { throw new ArgumentNullException(); } this.camera = camera;
this.canvas = canvas; canvas.MouseDown += this.mouseDownEvent;
canvas.MouseMove += this.mouseMoveEvent;
canvas.MouseUp += this.mouseUpEvent;
canvas.MouseWheel += this.mouseWheelEvent; SetCamera(camera.Position, camera.Target, camera.UpVector);
} public override void Unbind()
{
if (this.canvas != null && (!this.canvas.IsDisposed))
{
this.canvas.MouseDown -= this.mouseDownEvent;
this.canvas.MouseMove -= this.mouseMoveEvent;
this.canvas.MouseUp -= this.mouseUpEvent;
this.canvas.MouseWheel -= this.mouseWheelEvent;
this.canvas = null;
this.camera = null;
}
} void IMouseHandler.canvas_MouseWheel(object sender, MouseEventArgs e)
{
} void IMouseHandler.canvas_MouseDown(object sender, MouseEventArgs e)
{
this.lastBindingMouseButtons = this.BindingMouseButtons;
if ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None)
{
var control = sender as Control;
this.SetBounds(control.Width, control.Height); if (!cameraState.IsSameState(this.camera))
{
SetCamera(this.camera.Position, this.camera.Target, this.camera.UpVector);
} this._startPosition = GetArcBallPosition(e.X, e.Y); mouseDownFlag = true;
}
} private void SetBounds(int width, int height)
{
this._width = width; this._height = height;
_length = width > height ? width : height;
var rx = (width / ) / _length;
var ry = (height / ) / _length;
_radiusRadius = (float)(rx * rx + ry * ry);
} void IMouseHandler.canvas_MouseMove(object sender, MouseEventArgs e)
{
if (mouseDownFlag && ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None))
{
if (!cameraState.IsSameState(this.camera))
{
SetCamera(this.camera.Position, this.camera.Target, this.camera.UpVector);
} this._endPosition = GetArcBallPosition(e.X, e.Y);
var cosAngle = _startPosition.dot(_endPosition) / (_startPosition.length() * _endPosition.length());
if (cosAngle > 1.0f) { cosAngle = 1.0f; }
else if (cosAngle < -) { cosAngle = -1.0f; }
var angle = MouseSensitivity * (float)(Math.Acos(cosAngle) / Math.PI * );
_normalVector = _startPosition.cross(_endPosition).normalize();
if (!
((_normalVector.x == && _normalVector.y == && _normalVector.z == )
|| float.IsNaN(_normalVector.x) || float.IsNaN(_normalVector.y) || float.IsNaN(_normalVector.z)))
{
_startPosition = _endPosition; mat4 newRotation = glm.rotate(angle, _normalVector);
this.totalRotation = newRotation * totalRotation;
}
}
} private vec3 GetArcBallPosition(int x, int y)
{
float rx = (x - _width / ) / _length;
float ry = (_height / - y) / _length;
float zz = _radiusRadius - rx * rx - ry * ry;
float rz = (zz > ? (float)Math.Sqrt(zz) : 0.0f);
var result = new vec3(
rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorBack.x,
rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorBack.y,
rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorBack.z
);
//var position = new vec3(rx, ry, rz);
//var matrix = new mat3(_vectorRight, _vectorUp, _vectorBack);
//result = matrix * position; return result;
} void IMouseHandler.canvas_MouseUp(object sender, MouseEventArgs e)
{
if ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None)
{
mouseDownFlag = false;
}
} }

ArcBallManipulater

注意,在GetArcBallPosition(int x, int y);中,获取位置实际上是一个坐标变换的过程,所以可以用矩阵*向量实现。详见被注释掉的代码。

         private vec3 GetArcBallPosition(int x, int y)
{
float rx = (x - _width / ) / _length;
float ry = (_height / - y) / _length;
float zz = _radiusRadius - rx * rx - ry * ry;
float rz = (zz > ? (float)Math.Sqrt(zz) : 0.0f);
var result = new vec3(
rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorBack.x,
rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorBack.y,
rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorBack.z
);
// Get position using matrix * vector.
//var position = new vec3(rx, ry, rz);
//var matrix = new mat3(_vectorRight, _vectorUp, _vectorBack);
//result = matrix * position; return result;
}

2016-02-10

我已在CSharpGL中集成了最新的轨迹球代码。轨迹球只负责旋转。

 using GLM;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace CSharpGL.Objects.Cameras
{
/// <summary>
/// 用鼠标旋转模型。
/// </summary>
public class ArcBallRotator
{
vec3 _vectorCenterEye;
vec3 _vectorUp;
vec3 _vectorRight;
float _length, _radiusRadius;
CameraState cameraState = new CameraState();
mat4 totalRotation = mat4.identity();
vec3 _startPosition, _endPosition, _normalVector = new vec3(, , );
int _width;
int _height; float mouseSensitivity = 0.1f; public float MouseSensitivity
{
get { return mouseSensitivity; }
set { mouseSensitivity = value; }
} /// <summary>
/// 标识鼠标是否按下
/// </summary>
public bool MouseDownFlag { get; private set; } /// <summary>
///
/// </summary>
public ICamera Camera { get; set; } const string listenerName = "ArcBallRotator"; /// <summary>
/// 用鼠标旋转模型。
/// </summary>
/// <param name="camera">当前场景所用的摄像机。</param>
public ArcBallRotator(ICamera camera)
{
this.Camera = camera; SetCamera(camera.Position, camera.Target, camera.UpVector);
#if DEBUG
const string filename = "ArcBallRotator.log";
if (File.Exists(filename)) { File.Delete(filename); }
Debug.Listeners.Add(new TextWriterTraceListener(filename, listenerName));
Debug.WriteLine(DateTime.Now, listenerName);
Debug.Flush();
#endif
} private void SetCamera(vec3 position, vec3 target, vec3 up)
{
_vectorCenterEye = position - target;
_vectorCenterEye.Normalize();
_vectorUp = up;
_vectorRight = _vectorUp.cross(_vectorCenterEye);
_vectorRight.Normalize();
_vectorUp = _vectorCenterEye.cross(_vectorRight);
_vectorUp.Normalize(); this.cameraState.position = position;
this.cameraState.target = target;
this.cameraState.up = up;
} class CameraState
{
public vec3 position;
public vec3 target;
public vec3 up; public bool IsSameState(ICamera camera)
{
if (camera.Position != this.position) { return false; }
if (camera.Target != this.target) { return false; }
if (camera.UpVector != this.up) { return false; } return true;
}
} public void SetBounds(int width, int height)
{
this._width = width; this._height = height;
_length = width > height ? width : height;
var rx = (width / ) / _length;
var ry = (height / ) / _length;
_radiusRadius = (float)(rx * rx + ry * ry);
} /// <summary>
/// 必须先调用<see cref="SetBounds"/>()方法。
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public void MouseDown(int x, int y)
{
Debug.WriteLine("");
Debug.WriteLine("=================>MouseDown:", listenerName);
if (!cameraState.IsSameState(this.Camera))
{
SetCamera(this.Camera.Position, this.Camera.Target, this.Camera.UpVector);
Debug.WriteLine(string.Format(
"update camera state: {0}, {1}, {2}",
this.cameraState.position, this.cameraState.target, this.cameraState.up), listenerName);
} this._startPosition = GetArcBallPosition(x, y);
Debug.WriteLine(string.Format("Start position: {0}", this._startPosition), listenerName); MouseDownFlag = true; Debug.WriteLine("-------------------MouseDown end.", listenerName);
} private vec3 GetArcBallPosition(int x, int y)
{
var rx = (x - _width / ) / _length;
var ry = (_height / - y) / _length;
var zz = _radiusRadius - rx * rx - ry * ry;
var rz = (zz > ? Math.Sqrt(zz) : );
var result = new vec3(
(float)(rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorCenterEye.x),
(float)(rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorCenterEye.y),
(float)(rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorCenterEye.z)
);
return result;
} public void MouseMove(int x, int y)
{
if (MouseDownFlag)
{
Debug.WriteLine(" =================>MouseMove:", listenerName);
if (!cameraState.IsSameState(this.Camera))
{
SetCamera(this.Camera.Position, this.Camera.Target, this.Camera.UpVector);
Debug.WriteLine(string.Format(
" update camera state: {0}, {1}, {2}",
this.cameraState.position, this.cameraState.target, this.cameraState.up), listenerName);
} this._endPosition = GetArcBallPosition(x, y);
Debug.WriteLine(string.Format(
" End position: {0}", this._endPosition), listenerName);
var cosAngle = _startPosition.dot(_endPosition) / (_startPosition.Magnitude() * _endPosition.Magnitude());
if (cosAngle > ) { cosAngle = ; }
else if (cosAngle < -) { cosAngle = -; }
Debug.Write(string.Format(" cos angle: {0}", cosAngle), listenerName);
var angle = mouseSensitivity * (float)(Math.Acos(cosAngle) / Math.PI * );
Debug.WriteLine(string.Format(
", angle: {0}", angle), listenerName);
_normalVector = _startPosition.cross(_endPosition);
_normalVector.Normalize();
if ((_normalVector.x == && _normalVector.y == && _normalVector.z == )
|| float.IsNaN(_normalVector.x) || float.IsNaN(_normalVector.y) || float.IsNaN(_normalVector.z))
{
Debug.WriteLine(" no movement recorded.", listenerName);
}
else
{
Debug.WriteLine(string.Format(
" normal vector: {0}", _normalVector), listenerName);
_startPosition = _endPosition; mat4 newRotation = glm.rotate(angle, _normalVector);
Debug.WriteLine(string.Format(
" new rotation matrix: {0}", newRotation), listenerName);
this.totalRotation = newRotation * totalRotation;
Debug.WriteLine(string.Format(
" total rotation matrix: {0}", totalRotation), listenerName);
}
Debug.WriteLine(" -------------------MouseMove end.", listenerName);
}
} public void MouseUp(int x, int y)
{
Debug.WriteLine("=================>MouseUp:", listenerName);
MouseDownFlag = false;
Debug.WriteLine("-------------------MouseUp end.", listenerName);
Debug.WriteLine("");
Debug.Flush();
} public mat4 GetRotationMatrix()
{
return totalRotation;
}
}
}

ArcBallRotator

1. 轨迹球原理

【OpenGL(SharpGL)】支持任意相机可平移缩放的轨迹球实现-LMLPHP【OpenGL(SharpGL)】支持任意相机可平移缩放的轨迹球实现-LMLPHP

上面是我黑来的两张图,拿来说明轨迹球的原理。

看左边这个,网格代表绘制3D模型的窗口,上面放了个半球,这个球就是轨迹球。假设鼠标在网格上的某点A,过A点作网格所在平面的垂线,与半球相交于点P,P就是A在轨迹球上的投影。鼠标从A1点沿直线移动到A2点,对应着轨迹球上的点P1沿球面移动到了P2。那么,从球心O到P1和P2分别有两个向量OP1和OP2。OP1旋转到了OP2,我们就认为是模型也按照这个方式作同样的旋转。这就是轨迹球的旋转思路。

右边这个图没用上…

2. 轨迹球实现

实现轨迹球,首先要求出鼠标点A1、A2投影到轨迹球上的点P1、P2的坐标,然后计算两个向量A1P1和A2P2之间的夹角以及旋转轴,最后让模型按照求出的夹角和旋转轴,调用glRotate就可以了。

1) 计算投影点

在摄像机上应用轨迹球,才能实现适应任意位置摄像机的ArcBall类。

【OpenGL(SharpGL)】支持任意相机可平移缩放的轨迹球实现-LMLPHP

如图所示,红绿蓝三色箭头的交点是摄像机eye的位置,红色箭头指向center的位置,绿色箭头指向up的位置,蓝色箭头指向右侧。

说明:1.Up是可能在蓝色Right箭头的垂面内的任意方向的,这里我们要把它调整为与红色视线垂直的Up,即上图所示的Up。2.绿色和蓝色箭头组成的平面即为程序窗口所在位置,因为Eye就在这里嘛。而且Up指的就是屏幕正上方,Right指的就是屏幕正右方。3.显然轨迹球的半球在图中矩形所在的这一侧,球心就是Eye。

鼠标在Up和Right所在的平面移动,当它位于A点时,投影到轨迹球的点P。现在已知的是Eye、Center、原始Up、A点在屏幕上的坐标、向量Eye-P的长度、向量AP的长度。现在要求P点的坐标,只不过是一个数学问题了。

当然,开始的时候要设置相机位置。

         public void SetCamera(float eyex, float eyey, float eyez,
float centerx, float centery, float centerz,
float upx, float upy, float upz)
{
_vectorCenterEye = new Vertex(eyex - centerx, eyey - centery, eyez - centerz);
_vectorCenterEye.Normalize();
_vectorUp = new Vertex(upx, upy, upz);
_vectorRight = _vectorUp.VectorProduct(_vectorCenterEye);
_vectorRight.Normalize();
_vectorUp = _vectorCenterEye.VectorProduct(_vectorRight);
_vectorUp.Normalize();
}

根据鼠标在屏幕上的位置投影点的计算方法如下。

         private Vertex GetArcBallPosition(int x, int y)
{
var rx = (x - _width / ) / _length;
var ry = (_height / - y) / _length;
var zz = _radiusRadius - rx * rx - ry * ry;
var rz = (zz > ? Math.Sqrt(zz) : );
var result = new Vertex(
(float)(rx * _vectorRight.X + ry * _vectorUp.X + rz * _vectorCenterEye.X),
(float)(rx * _vectorRight.Y + ry * _vectorUp.Y + rz * _vectorCenterEye.Y),
(float)(rx * _vectorRight.Z + ry * _vectorUp.Z + rz * _vectorCenterEye.Z)
);
return result;
}

这里主要应用了向量的思想,向量(Eye-P) = 向量(Eye-A) + 向量(A-P)。而向量(Eye-A)和向量(A-P)都是可以通过单位长度的Up、Center-Eye和Right向量求得的。

2) 计算夹角和旋转轴

首先,设置鼠标按下事件

         public void MouseDown(int x, int y)
{
this._startPosition = GetArcBallPosition(x, y); mouseDownFlag = true;
}

然后,设置鼠标移动事件。此时P1P2两个点都有了,旋转轴和夹角就都可以计算了。

         public void MouseMove(int x, int y)
{
if (mouseDownFlag)
{
this._endPosition = GetArcBallPosition(x, y);
var cosAngle = _startPosition.ScalarProduct(_endPosition) / (_startPosition.Magnitude() * _endPosition.Magnitude());
if (cosAngle > ) { cosAngle = ; }
else if (cosAngle < -) { cosAngle = -; }
var angle = * (float)(Math.Acos(cosAngle) / Math.PI * );
System.Threading.Interlocked.Exchange(ref _angle, angle);
_normalVector = _startPosition.VectorProduct(_endPosition);
_startPosition = _endPosition;
}
}

然后,设置鼠标弹起的事件。

         public void MouseUp(int x, int y)
{
mouseDownFlag = false;
}

在使用opengl(sharpgl)绘制的时候,调用

         public void TransformMatrix(OpenGL gl)
{
gl.PushMatrix();
gl.LoadIdentity();
gl.Rotate( * _angle, _normalVector.X, _normalVector.Y, _normalVector.Z);
System.Threading.Interlocked.Exchange(ref _angle, );
gl.MultMatrix(_lastTransform);
gl.GetDouble(Enumerations.GetTarget.ModelviewMatix, _lastTransform);
gl.PopMatrix();
gl.Translate(_translateX, _translateY, _translateZ);
gl.MultMatrix(_lastTransform);
gl.Scale(Scale, Scale, Scale);
}

3. 额外功能实现

缩放很容易实现,直接设置Scale属性即可。

沿着屏幕上下左右前后地移动,则需要参照着camera的方向动了。

         public void GoUp(float interval)
{
this._translateX += this._vectorUp.X * interval;
this._translateY += this._vectorUp.Y * interval;
this._translateZ += this._vectorUp.Z * interval;
}

其余方向与此类似,不再浪费篇幅。

工程源代码在此。(http://files.cnblogs.com/bitzhuwei/Arcball6662014-02-07_20-07-00.rar

05-07 15:57