【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 }
注意,在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 }
其余方向与此类似,不再浪费篇幅。
工程源代码在此。(https://files.cnblogs.com/bitzhuwei/Arcball6662014-02-07_20-07-00.rar)
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |