模型自由旋转的数学基础
我们用鼠标实现模型的旋转,就好像手握一个包含模型的虚拟球一样。按一下鼠标,即在这个虚拟球上确定了一点,而拖动鼠标就是移动那个点,这样就实现了对虚拟球的旋转,同时达到旋转模型的目的。
|
这个虚拟球的中心位于显示屏的中心,这样球的一半则位于显示屏以外(外半球,如图1所示)。我们用鼠标点击的点将定义为外半球上的点。这种映射关系的数学定义为:
|
其中(x,y)是以球心为原点的屏幕坐标,R为球的半径。
接下来要做的就是在球上给定两个点后(起始点和终点)怎样确定旋转的轴和角度。从图2中可以看出:旋转轴是两个鼠标矢量(m1和m2)所张成的平面的法向量,所以可以通过求m1和m2的叉乘得到,即:
Axis = m1 x m2 (式2) |
而旋转角度就是m1和m2之间的夹角a,因此:
a = acos (m1 * m2) (式3) |
|
在实际应用中,我们更习惯取a的两倍值进行旋转。因为这样将更有效地旋转模型。如果用鼠标点击视图的左中边缘,然后拖动至视图的右中边缘,则可实现模型以y轴为旋转轴的360度旋转。
从图3可以看出:在旋转的过程中,两个弧(R1和R2)的合成所形成的旋转弧等于R1的起始点和R2的终点形成的旋转弧。即意味着我们定义的虚拟球的旋转运动只决定于起始点和终点。
编程实现自由旋转的方法和经验
1、首先建立一个虚拟球类
用面向对象的方法来解决问题,能使解决方案有很好的可移植性和可维护性。而VC++是功能强大的面向对象编程的工具,所以我们使用VC++面向对象程序设计的方案来实现自由旋转功能。虚拟球类的声明如下:
1、首先建立一个虚拟球类
用面向对象的方法来解决问题,能使解决方案有很好的可移植性和可维护性。而VC++是功能强大的面向对象编程的工具,所以我们使用VC++面向对象程序设计的方案来实现自由旋转功能。虚拟球类的声明如下:
class VirtualBall { protected: void _mapToSphere(const Point2fT* NewPt, Vector3fT* NewVec) const; public: //构造和析构函数 VirtualBall(GLfloat NewWidth, GLfloat NewHeight); ~VirtualBall() { }; //设置边界, 当窗口大小改变时,使虚拟球与窗口大小相适应 void setBounds(GLfloat NewWidth, GLfloat NewHeight) void click(const Point2fT* NewPt);// 鼠标按下,映射起始点到虚拟球 //鼠标拖动,第二个鼠标坐标在这里得到更新,并映射到虚拟球上,计算旋转 //轴的向量和夹角的信息,将它们保存到一个四元数NewRot中(前3个元素为 //坐标信息,最后一个元素为关于夹角的信息,其实就是两个向量的点乘) void drag(const Point2fT* NewPt, Quat4fT* NewRot); protected: Vector3fT StVec; //保存鼠标点击时的向量(起始点) Vector3fT EnVec; //保存拖动时的向量(终点) GLfloat AdjustWidth; //setBounds函数用其来调整窗口 GLfloat AdjustHeight; } |
2、把鼠标坐标映射为虚拟球上的坐标
通过虚拟球的旋转来达到旋转模型的目的,关键在于把视图中鼠标点击和拖动的坐标映射为虚拟球上的坐标。
为此,我们首先简单的把鼠标点击和拖动的范围[0~width),[0~height)映射到 [-1~1],[1~ -1](在映射中我们颠倒了y坐标的符号,不然OpenGL中得不到正确的结果)。这样做可以使数学计算变得简单些,其映射如下:
MousePt.X = ((MousePt.X / ((Width – 1) / 2)) – 1); MousePt.Y = -((MousePt.Y / ((Height – 1) / 2)) – 1); |
其次,计算鼠标矢量,将鼠标坐标映射到虚拟球上,可以根据式1的定义完成这一步工作。
3、些相关变量的设定
为实现旋转我们还需要一些变量:
Matrix4fT Transform // 最终的变换,4*4矩阵,初始化为单位矩阵 Matrix3fT LastRot // 上一次的旋转,3*3矩阵,需要它是因为旋转的结果是要叠加起来的 Matrix3fT ThisRot //这次的旋转,3*3矩阵。 Point2fT MousePt; // 当前的鼠标坐标 bool isClicked = false; // 鼠标按下的标识 bool isRClicked = false; // 右键点击的标识 bool isDragging = false; //鼠标拖动的标识 |
其中Transform是我们的最终变换结果,LastRot是上一次鼠标拖动得到的旋转结果,而ThisRot是当前鼠标拖动的结果。它们都被初始化为单位矩阵。
当我们点击鼠标时,我们从单位旋转矩阵开始旋转。当拖动鼠标时,我们计算从初始点到拖动点的旋转。尽管我们用这信息旋转屏幕上的模型,但值得注意的是我 们并不是真的旋转虚拟球自身。所以要得到累积的旋转结果,我们必须自己想办法,这也就是引入LastRot的原因。如果不累积旋转,模型就会在我们点击鼠 标时突然跑回到原始的状态。例如,如果关于X轴旋转90度后再旋转45度,希望得到135度的结果,但实际上得到的是45度。在下一次点击鼠标时,又会回 到原始的0度状态。
其他的变量,我们要做的就是在适当的时间和地点更新它们。虚拟球需要在窗口大小改变时重新设置它的边界; MousePt在鼠标点击和拖动时得到更新;isClicked和isRClicked分别标识鼠标的左键和右键是否按下,isClicked用来判断是 否处于按下和拖动状态,我们用isRClicked来重置所有的旋转,使其回到单位矩阵状态。
4、更新旋转矩阵
有了以上变量的更新,接下来就是根据这些更新,实现旋转矩阵的更新:
void CRenderView::OnTimer(UINT nIDEvent) { if(m_Completed) { m_Completed = false; if (isRClicked) // 如果点击右键,重置旋转 { Matrix3fSetIdentity(&LastRot); //把LastRot重置为单位矩阵 Matrix3fSetIdentity(&ThisRot); //把ThisRot重置为单位矩阵 Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot); } if (!isDragging) // 没有拖动 { if (isClicked) // 第一次点击 { isDragging = true; // 为拖动作准备 LastRot = ThisRot; VirtualBall.click(&MousePt); } } // 更新起始点,为拖动作准备 else { if (isClicked) // 鼠标仍然被按下,说明仍处于拖动状态 { Quat4fT ThisQuat; //一个四元数,用来存旋转的信息 ArcBall.drag(&MousePt, &ThisQuat); //将四元数转化为旋转矩阵 Matrix3fSetRotationFromQuat4f(&ThisRot, &ThisQuat); Matrix3fMulMatrix3f(&ThisRot, &LastRot); //累积旋转结果 //得到我们最终的旋转结果 Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot); } else //没有拖动的 isDragging = false; } m_OpenGLDisplay.DisplayScene(m_p3DModel);// m_Completed = true; } CView::OnTimer(nIDEvent); } |
其中将四元数转化为旋转矩阵的函数为:
static void Matrix3fSetRotationFromQuat4f(Matrix3fT* NewObj, const Quat4fT* q1) { GLfloat n, s; GLfloat xs, ys, zs; GLfloat wx, wy, wz; GLfloat xx, xy, xz; GLfloat yy, yz, zz; assert(NewObj && q1); n = (q1->s.X * q1->s.X) + (q1->s.Y * q1->s.Y) + (q1->s.Z * q1->s.Z) + (q1->s.W * q1->s.W); s = (n > 0.0f) ? (2.0f / n) : 0.0f; xs = q1->s.X * s; ys = q1->s.Y * s; zs = q1->s.Z * s; wx = q1->s.W * xs; wy = q1->s.W * ys; wz = q1->s.W * zs; xx = q1->s.X * xs; xy = q1->s.X * ys; xz = q1->s.X * zs; yy = q1->s.Y * ys; yz = q1->s.Y * zs; zz = q1->s.Z * zs; NewObj->s.XX = 1.0f - (yy + zz); NewObj->s.YX = xy - wz; NewObj->s.ZX = xz + wy; NewObj->s.XY = xy + wz; NewObj->s.YY = 1.0f - (xx + zz); NewObj->s.ZY = yz - wx; NewObj->s.XZ = xz - wy; NewObj->s.YZ = yz + wx; NewObj->s.ZZ = 1.0f - (xx + yy); } |
最后,把变换的结果应用于从3DS文件中读入的模型:
glPushMatrix(); glMultMatrixf(Transform.M); //将旋转的矩阵作用于模型上 glBegin(DrawingMode); ………//此处为画模型的地方,即画模型各个面的地方 glEnd(); glPopMatrix(); |