【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 }
注意,在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 }
1. 軌跡球原理
上面是我黑來的兩張圖,拿來說明軌跡球的原理。
看左邊這個,網格代表繪制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)

![clip_image003[4] clip_image003[4]](/image/aHR0cHM6Ly9pbWFnZXMwLmNuYmxvZ3MuY29tL2Jsb2cvMzgzMTkxLzIwMTQwMi8wNzEzNDgzMTYxMzYwMjcuanBn.png)
![clip_image004[4] clip_image004[4]](/image/aHR0cHM6Ly9pbWFnZXMwLmNuYmxvZ3MuY29tL2Jsb2cvMzgzMTkxLzIwMTQwMi8wNzEzNDgzMzEwOTI3NjkuanBn.png)