【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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值