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

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

(本文PDF版在这里。)

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

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

2016-07-08

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

  1     /// <summary>
  2     /// Rotate model using arc-ball method.
  3     /// </summary>
  4     public class ArcBallManipulater : Manipulater, IMouseHandler
  5     {
  6 
  7         private ICamera camera;
  8         private GLCanvas canvas;
  9 
 10         private MouseEventHandler mouseDownEvent;
 11         private MouseEventHandler mouseMoveEvent;
 12         private MouseEventHandler mouseUpEvent;
 13         private MouseEventHandler mouseWheelEvent;
 14 
 15         private vec3 _vectorRight;
 16         private vec3 _vectorUp;
 17         private vec3 _vectorBack;
 18         private float _length, _radiusRadius;
 19         private CameraState cameraState = new CameraState();
 20         private mat4 totalRotation = mat4.identity();
 21         private vec3 _startPosition, _endPosition, _normalVector = new vec3(0, 1, 0);
 22         private int _width;
 23         private int _height;
 24         private bool mouseDownFlag;
 25 
 26         public float MouseSensitivity { get; set; }
 27 
 28         public MouseButtons BindingMouseButtons { get; set; }
 29         private MouseButtons lastBindingMouseButtons;
 30 
 31         /// <summary>
 32         /// Rotate model using arc-ball method.
 33         /// </summary>
 34         /// <param name="bindingMouseButtons"></param>
 35         public ArcBallManipulater(MouseButtons bindingMouseButtons = MouseButtons.Left)
 36         {
 37             this.MouseSensitivity = 0.1f;
 38             this.BindingMouseButtons = bindingMouseButtons;
 39 
 40             this.mouseDownEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseDown);
 41             this.mouseMoveEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseMove);
 42             this.mouseUpEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseUp);
 43             this.mouseWheelEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseWheel);
 44         }
 45 
 46         private void SetCamera(vec3 position, vec3 target, vec3 up)
 47         {
 48             _vectorBack = (position - target).normalize();
 49             _vectorRight = up.cross(_vectorBack).normalize();
 50             _vectorUp = _vectorBack.cross(_vectorRight).normalize();
 51 
 52             this.cameraState.position = position;
 53             this.cameraState.target = target;
 54             this.cameraState.up = up;
 55         }
 56 
 57         class CameraState
 58         {
 59             public vec3 position;
 60             public vec3 target;
 61             public vec3 up;
 62 
 63             public bool IsSameState(ICamera camera)
 64             {
 65                 if (camera.Position != this.position) { return false; }
 66                 if (camera.Target != this.target) { return false; }
 67                 if (camera.UpVector != this.up) { return false; }
 68 
 69                 return true;
 70             }
 71         }
 72 
 73         public mat4 GetRotationMatrix()
 74         {
 75             return totalRotation;
 76         }
 77 
 78         public override void Bind(ICamera camera, GLCanvas canvas)
 79         {
 80             if (camera == null || canvas == null) { throw new ArgumentNullException(); }
 81 
 82             this.camera = camera;
 83             this.canvas = canvas;
 84 
 85             canvas.MouseDown += this.mouseDownEvent;
 86             canvas.MouseMove += this.mouseMoveEvent;
 87             canvas.MouseUp += this.mouseUpEvent;
 88             canvas.MouseWheel += this.mouseWheelEvent;
 89 
 90             SetCamera(camera.Position, camera.Target, camera.UpVector);
 91         }
 92 
 93         public override void Unbind()
 94         {
 95             if (this.canvas != null && (!this.canvas.IsDisposed))
 96             {
 97                 this.canvas.MouseDown -= this.mouseDownEvent;
 98                 this.canvas.MouseMove -= this.mouseMoveEvent;
 99                 this.canvas.MouseUp -= this.mouseUpEvent;
100                 this.canvas.MouseWheel -= this.mouseWheelEvent;
101                 this.canvas = null;
102                 this.camera = null;
103             }
104         }
105 
106         void IMouseHandler.canvas_MouseWheel(object sender, MouseEventArgs e)
107         {
108         }
109 
110         void IMouseHandler.canvas_MouseDown(object sender, MouseEventArgs e)
111         {
112             this.lastBindingMouseButtons = this.BindingMouseButtons;
113             if ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None)
114             {
115                 var control = sender as Control;
116                 this.SetBounds(control.Width, control.Height);
117 
118                 if (!cameraState.IsSameState(this.camera))
119                 {
120                     SetCamera(this.camera.Position, this.camera.Target, this.camera.UpVector);
121                 }
122 
123                 this._startPosition = GetArcBallPosition(e.X, e.Y);
124 
125                 mouseDownFlag = true;
126             }
127         }
128 
129         private void SetBounds(int width, int height)
130         {
131             this._width = width; this._height = height;
132             _length = width > height ? width : height;
133             var rx = (width / 2) / _length;
134             var ry = (height / 2) / _length;
135             _radiusRadius = (float)(rx * rx + ry * ry);
136         }
137 
138         void IMouseHandler.canvas_MouseMove(object sender, MouseEventArgs e)
139         {
140             if (mouseDownFlag && ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None))
141             {
142                 if (!cameraState.IsSameState(this.camera))
143                 {
144                     SetCamera(this.camera.Position, this.camera.Target, this.camera.UpVector);
145                 }
146 
147                 this._endPosition = GetArcBallPosition(e.X, e.Y);
148                 var cosAngle = _startPosition.dot(_endPosition) / (_startPosition.length() * _endPosition.length());
149                 if (cosAngle > 1.0f) { cosAngle = 1.0f; }
150                 else if (cosAngle < -1) { cosAngle = -1.0f; }
151                 var angle = MouseSensitivity * (float)(Math.Acos(cosAngle) / Math.PI * 180);
152                 _normalVector = _startPosition.cross(_endPosition).normalize();
153                 if (!
154                     ((_normalVector.x == 0 && _normalVector.y == 0 && _normalVector.z == 0)
155                     || float.IsNaN(_normalVector.x) || float.IsNaN(_normalVector.y) || float.IsNaN(_normalVector.z)))
156                 {
157                     _startPosition = _endPosition;
158 
159                     mat4 newRotation = glm.rotate(angle, _normalVector);
160                     this.totalRotation = newRotation * totalRotation;
161                 }
162             }
163         }
164 
165         private vec3 GetArcBallPosition(int x, int y)
166         {
167             float rx = (x - _width / 2) / _length;
168             float ry = (_height / 2 - y) / _length;
169             float zz = _radiusRadius - rx * rx - ry * ry;
170             float rz = (zz > 0 ? (float)Math.Sqrt(zz) : 0.0f);
171             var result = new vec3(
172                 rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorBack.x,
173                 rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorBack.y,
174                 rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorBack.z
175                 );
176             //var position = new vec3(rx, ry, rz);
177             //var matrix = new mat3(_vectorRight, _vectorUp, _vectorBack);
178             //result = matrix * position;
179 
180             return result;
181         }
182 
183         void IMouseHandler.canvas_MouseUp(object sender, MouseEventArgs e)
184         {
185             if ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None)
186             {
187                 mouseDownFlag = false;
188             }
189         }
190 
191     }
ArcBallManipulater

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

 1         private vec3 GetArcBallPosition(int x, int y)
 2         {
 3             float rx = (x - _width / 2) / _length;
 4             float ry = (_height / 2 - y) / _length;
 5             float zz = _radiusRadius - rx * rx - ry * ry;
 6             float rz = (zz > 0 ? (float)Math.Sqrt(zz) : 0.0f);
 7             var result = new vec3(
 8                 rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorBack.x,
 9                 rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorBack.y,
10                 rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorBack.z
11                 );
12             // Get position using matrix * vector.
13             //var position = new vec3(rx, ry, rz);
14             //var matrix = new mat3(_vectorRight, _vectorUp, _vectorBack);
15             //result = matrix * position;
16 
17             return result;
18         }

2016-02-10

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

  1 using GLM;
  2 using System;
  3 using System.Collections.Generic;
  4 using System.Diagnostics;
  5 using System.Drawing;
  6 using System.IO;
  7 using System.Linq;
  8 using System.Text;
  9 using System.Threading.Tasks;
 10 
 11 namespace CSharpGL.Objects.Cameras
 12 {
 13     /// <summary>
 14     /// 用鼠标旋转模型。
 15     /// </summary>
 16     public class ArcBallRotator
 17     {
 18         vec3 _vectorCenterEye;
 19         vec3 _vectorUp;
 20         vec3 _vectorRight;
 21         float _length, _radiusRadius;
 22         CameraState cameraState = new CameraState();
 23         mat4 totalRotation = mat4.identity();
 24         vec3 _startPosition, _endPosition, _normalVector = new vec3(0, 1, 0);
 25         int _width;
 26         int _height;
 27 
 28         float mouseSensitivity = 0.1f;
 29 
 30         public float MouseSensitivity
 31         {
 32             get { return mouseSensitivity; }
 33             set { mouseSensitivity = value; }
 34         }
 35 
 36         /// <summary>
 37         /// 标识鼠标是否按下
 38         /// </summary>
 39         public bool MouseDownFlag { get; private set; }
 40 
 41         /// <summary>
 42         /// 
 43         /// </summary>
 44         public ICamera Camera { get; set; }
 45 
 46 
 47         const string listenerName = "ArcBallRotator";
 48 
 49         /// <summary>
 50         /// 用鼠标旋转模型。
 51         /// </summary>
 52         /// <param name="camera">当前场景所用的摄像机。</param>
 53         public ArcBallRotator(ICamera camera)
 54         {
 55             this.Camera = camera;
 56 
 57             SetCamera(camera.Position, camera.Target, camera.UpVector);
 58 #if DEBUG
 59             const string filename = "ArcBallRotator.log";
 60             if (File.Exists(filename)) { File.Delete(filename); }
 61             Debug.Listeners.Add(new TextWriterTraceListener(filename, listenerName));
 62             Debug.WriteLine(DateTime.Now, listenerName);
 63             Debug.Flush();
 64 #endif
 65         }
 66 
 67         private void SetCamera(vec3 position, vec3 target, vec3 up)
 68         {
 69             _vectorCenterEye = position - target;
 70             _vectorCenterEye.Normalize();
 71             _vectorUp = up;
 72             _vectorRight = _vectorUp.cross(_vectorCenterEye);
 73             _vectorRight.Normalize();
 74             _vectorUp = _vectorCenterEye.cross(_vectorRight);
 75             _vectorUp.Normalize();
 76 
 77             this.cameraState.position = position;
 78             this.cameraState.target = target;
 79             this.cameraState.up = up;
 80         }
 81 
 82         class CameraState
 83         {
 84             public vec3 position;
 85             public vec3 target;
 86             public vec3 up;
 87 
 88             public bool IsSameState(ICamera camera)
 89             {
 90                 if (camera.Position != this.position) { return false; }
 91                 if (camera.Target != this.target) { return false; }
 92                 if (camera.UpVector != this.up) { return false; }
 93 
 94                 return true;
 95             }
 96         }
 97 
 98         public void SetBounds(int width, int height)
 99         {
100             this._width = width; this._height = height;
101             _length = width > height ? width : height;
102             var rx = (width / 2) / _length;
103             var ry = (height / 2) / _length;
104             _radiusRadius = (float)(rx * rx + ry * ry);
105         }
106 
107         /// <summary>
108         /// 必须先调用<see cref="SetBounds"/>()方法。
109         /// </summary>
110         /// <param name="x"></param>
111         /// <param name="y"></param>
112         public void MouseDown(int x, int y)
113         {
114             Debug.WriteLine("");
115             Debug.WriteLine("=================>MouseDown:", listenerName);
116             if (!cameraState.IsSameState(this.Camera))
117             {
118                 SetCamera(this.Camera.Position, this.Camera.Target, this.Camera.UpVector);
119                 Debug.WriteLine(string.Format(
120                     "update camera state: {0}, {1}, {2}",
121                     this.cameraState.position, this.cameraState.target, this.cameraState.up), listenerName);
122             }
123 
124             this._startPosition = GetArcBallPosition(x, y);
125             Debug.WriteLine(string.Format("Start position: {0}", this._startPosition), listenerName);
126 
127             MouseDownFlag = true;
128 
129             Debug.WriteLine("-------------------MouseDown end.", listenerName);
130         }
131 
132         private vec3 GetArcBallPosition(int x, int y)
133         {
134             var rx = (x - _width / 2) / _length;
135             var ry = (_height / 2 - y) / _length;
136             var zz = _radiusRadius - rx * rx - ry * ry;
137             var rz = (zz > 0 ? Math.Sqrt(zz) : 0);
138             var result = new vec3(
139                 (float)(rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorCenterEye.x),
140                 (float)(rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorCenterEye.y),
141                 (float)(rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorCenterEye.z)
142                 );
143             return result;
144         }
145 
146 
147         public void MouseMove(int x, int y)
148         {
149             if (MouseDownFlag)
150             {
151                 Debug.WriteLine("    =================>MouseMove:", listenerName);
152                 if (!cameraState.IsSameState(this.Camera))
153                 {
154                     SetCamera(this.Camera.Position, this.Camera.Target, this.Camera.UpVector);
155                     Debug.WriteLine(string.Format(
156                         "    update camera state: {0}, {1}, {2}",
157                         this.cameraState.position, this.cameraState.target, this.cameraState.up), listenerName);
158                 }
159 
160                 this._endPosition = GetArcBallPosition(x, y);
161                 Debug.WriteLine(string.Format(
162                     "    End position: {0}", this._endPosition), listenerName);
163                 var cosAngle = _startPosition.dot(_endPosition) / (_startPosition.Magnitude() * _endPosition.Magnitude());
164                 if (cosAngle > 1) { cosAngle = 1; }
165                 else if (cosAngle < -1) { cosAngle = -1; }
166                 Debug.Write(string.Format("    cos angle: {0}", cosAngle), listenerName);
167                 var angle = mouseSensitivity * (float)(Math.Acos(cosAngle) / Math.PI * 180);
168                 Debug.WriteLine(string.Format(
169                     ", angle: {0}", angle), listenerName);
170                 _normalVector = _startPosition.cross(_endPosition);
171                 _normalVector.Normalize();
172                 if ((_normalVector.x == 0 && _normalVector.y == 0 && _normalVector.z == 0)
173                     || float.IsNaN(_normalVector.x) || float.IsNaN(_normalVector.y) || float.IsNaN(_normalVector.z))
174                 {
175                     Debug.WriteLine("    no movement recorded.", listenerName);
176                 }
177                 else
178                 {
179                     Debug.WriteLine(string.Format(
180                         "    normal vector: {0}", _normalVector), listenerName);
181                     _startPosition = _endPosition;
182 
183                     mat4 newRotation = glm.rotate(angle, _normalVector);
184                     Debug.WriteLine(string.Format(
185                         "    new rotation matrix:   {0}", newRotation), listenerName);
186                     this.totalRotation = newRotation * totalRotation;
187                     Debug.WriteLine(string.Format(
188                         "    total rotation matrix: {0}", totalRotation), listenerName);
189                 }
190                 Debug.WriteLine("    -------------------MouseMove end.", listenerName);
191             }
192         }
193 
194         public void MouseUp(int x, int y)
195         {
196             Debug.WriteLine("=================>MouseUp:", listenerName);
197             MouseDownFlag = false;
198             Debug.WriteLine("-------------------MouseUp end.", listenerName);
199             Debug.WriteLine("");
200             Debug.Flush();
201         }
202 
203         public mat4 GetRotationMatrix()
204         {
205             return totalRotation;
206         }
207     }
208 }
ArcBallRotator

1. 轨迹球原理

clip_image003[4]clip_image004[4]

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

看左边这个,网格代表绘制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类。

在相机上应用轨迹球

如图所示,红绿蓝三色箭头的交点是摄像机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点的坐标,只不过是一个数学问题了。

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

 1         public void SetCamera(float eyex, float eyey, float eyez,
 2             float centerx, float centery, float centerz,
 3             float upx, float upy, float upz)
 4         {
 5             _vectorCenterEye = new Vertex(eyex - centerx, eyey - centery, eyez - centerz);
 6             _vectorCenterEye.Normalize();
 7             _vectorUp = new Vertex(upx, upy, upz);
 8             _vectorRight = _vectorUp.VectorProduct(_vectorCenterEye);
 9             _vectorRight.Normalize();
10             _vectorUp = _vectorCenterEye.VectorProduct(_vectorRight);
11             _vectorUp.Normalize();
12         }

  

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

 1         private Vertex GetArcBallPosition(int x, int y)
 2         {
 3             var rx = (x - _width / 2) / _length;
 4             var ry = (_height / 2 - y) / _length;
 5             var zz = _radiusRadius - rx * rx - ry * ry;
 6             var rz = (zz > 0 ? Math.Sqrt(zz) : 0);
 7             var result = new Vertex(
 8                 (float)(rx * _vectorRight.X + ry * _vectorUp.X + rz * _vectorCenterEye.X),
 9                 (float)(rx * _vectorRight.Y + ry * _vectorUp.Y + rz * _vectorCenterEye.Y),
10                 (float)(rx * _vectorRight.Z + ry * _vectorUp.Z + rz * _vectorCenterEye.Z)
11                 );
12             return result;
13         }

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

2) 计算夹角和旋转轴

首先,设置鼠标按下事件

1         public void MouseDown(int x, int y)
2         {
3             this._startPosition = GetArcBallPosition(x, y);
4 
5             mouseDownFlag = true;
6         }

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

 1         public void MouseMove(int x, int y)
 2         {
 3             if (mouseDownFlag)
 4             {
 5                 this._endPosition = GetArcBallPosition(x, y);
 6                 var cosAngle = _startPosition.ScalarProduct(_endPosition) / (_startPosition.Magnitude() * _endPosition.Magnitude());
 7                 if (cosAngle > 1) { cosAngle = 1; }
 8                 else if (cosAngle < -1) { cosAngle = -1; }
 9                 var angle = 10 * (float)(Math.Acos(cosAngle) / Math.PI * 180);
10                 System.Threading.Interlocked.Exchange(ref _angle, angle);
11                 _normalVector = _startPosition.VectorProduct(_endPosition);
12                 _startPosition = _endPosition;
13             }
14         }

  

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

1         public void MouseUp(int x, int y)
2         {
3             mouseDownFlag = false;
4         }

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

 1         public void TransformMatrix(OpenGL gl)
 2         {
 3             gl.PushMatrix();
 4             gl.LoadIdentity();
 5             gl.Rotate(2 * _angle, _normalVector.X, _normalVector.Y, _normalVector.Z);
 6             System.Threading.Interlocked.Exchange(ref _angle, 0);
 7             gl.MultMatrix(_lastTransform);
 8             gl.GetDouble(Enumerations.GetTarget.ModelviewMatix, _lastTransform);
 9             gl.PopMatrix();
10             gl.Translate(_translateX, _translateY, _translateZ);
11             gl.MultMatrix(_lastTransform);
12             gl.Scale(Scale, Scale, Scale);
13         }

3. 额外功能实现

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

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

1         public void GoUp(float interval)
2         {
3             this._translateX += this._vectorUp.X * interval;
4             this._translateY += this._vectorUp.Y * interval;
5             this._translateZ += this._vectorUp.Z * interval;
6         }

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

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

原文地址:https://www.cnblogs.com/bitzhuwei/p/arcball_4_all_camera.html