【OpenGL(SharpGL)】支持任意相機可平移縮放的軌跡球實現

【OpenGL(SharpGL)】支持任意相機可平移縮放的軌跡球實現

閱讀目錄(Content)

2016-07-08
2016-02-10
1. 軌跡球原理
2. 軌跡球實現
    1) 計算投影點
    2) 計算夾角和旋轉軸
    3. 額外功能實現

【OpenGL(SharpGL)】支持任意相機可平移縮放的軌跡球

(本文PDF版在這裏。)

在3D程序中,軌跡球(ArcBall)可以讓你只用鼠標來控制模型(旋轉),便於觀察。在這裏(http://www.yakergong.net/nehe/ )有nehe的軌跡球教程。

本文提供一個本人編寫的軌跡球類(ArcBall.cs),它可以直接應用到任何camera下,還可以同時實現縮放和平移。工程源代碼在文末。
回到頂部(go to top)
2016-07-08

再次更新了軌跡球代碼,重命名爲ArcBallManipulater。
複製代碼

1 ///
2 /// Rotate model using arc-ball method.
3 ///
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 ///
32 /// Rotate model using arc-ball method.
33 ///
34 ///
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 }

複製代碼

回到頂部(go to top)
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 ///
14 /// 用鼠標旋轉模型。
15 ///
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 ///
37 /// 標識鼠標是否按下
38 ///
39 public bool MouseDownFlag { get; private set; }
40
41 ///
42 ///
43 ///
44 public ICamera Camera { get; set; }
45
46
47 const string listenerName = “ArcBallRotator”;
48
49 ///
50 /// 用鼠標旋轉模型。
51 ///
52 /// 當前場景所用的攝像機。
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 ///
108 /// 必須先調用()方法。
109 ///
110 ///
111 ///
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 }

複製代碼

回到頂部(go to top)

  1. 軌跡球原理

clip_image003[4]clip_image004[4]

上面是我黑來的兩張圖,拿來說明軌跡球的原理。

看左邊這個,網格代表繪製3D模型的窗口,上面放了個半球,這個球就是軌跡球。假設鼠標在網格上的某點A,過A點作網格所在平面的垂線,與半球相交於點P,P就是A在軌跡球上的投影。鼠標從A1點沿直線移動到A2點,對應着軌跡球上的點P1沿球面移動到了P2。那麼,從球心O到P1和P2分別有兩個向量OP1和OP2。OP1旋轉到了OP2,我們就認爲是模型也按照這個方式作同樣的旋轉。這就是軌跡球的旋轉思路。

右邊這個圖沒用上…
回到頂部(go to top)
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 }

複製代碼

  1. 額外功能實現

縮放很容易實現,直接設置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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章