【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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM