摘要 简述如何在OpenGL中, 读入和显示3DS文件中的模型,并着重阐述通过鼠标拖动对其进行自由旋转的数学基础和编程实现的方法。
关键词 OpenGL 3DS文件格式 VC++ 自由旋转
现在已经有很多论文和书籍提到在OpenGL中实现读入和显示3DS文件中的模型。但是在很多场合,仅读入和显示是不够的。我们需要从各个角度观察模 型,以便更好地理解模型的形态,形成更为直观的感性认识。例如,在医学髁上骨折诊断中,如果把骨折后,断骨错位旋转的情况用三维模型模拟出来,并仅用鼠标 的拖动就能实现从任何角度观看骨折的情况,这将对医生做出正确的诊断大有裨益。这也是我们为何考虑实现此项功能的初衷。本文将简要介绍3DS文件格式,怎 样读入和显示模型,而重点放在通过鼠标拖动实现模型自由旋转的数学基础和编程实现的方法和经验。
3DS文件的格式以及读入和显示文件中模型的一些经验.
3DS文件是由许多块(chunk)组成的(大块中镶嵌子块)。由于至今为止,没有一个官方的文献说明其格式,所以还有很多未知的块。不过这并不影响我 们读入3DS文件中的模型。因为我们在读入时,可以根据自己的需要选择性地读入自己需要的块,而忽略掉那些不感兴趣或未知的块。这正是块结构给我们带来的 好处。
一个块由块信息和块数据组成。块信息又由块的ID(两个字节长的标识,如4D4D)和块的长度(四个字节,其实也就是下一个块 的偏移字节数)组成。用VC++以十六进制方式打开一3DS文件可以很清楚的看到其结构。在读入这种块结构(大块中嵌套小块,而块的结构固定)的文件时, 完全可以用递归的方法实现,而返回上一级(子块读完,返回父块)的条件则是当前已经读入的块的字节数是否等于块的长度。从父块转向读入其子块,则可用 switch语句实现,通过子块的ID判断进入哪个分支。
由于在网上有很多现成的这类程序,所以完全可是找一个类封装的比较好的程序,将其移植到自己的工程中就行了。当然需要做一些小小的改动,比如根据自己的需要修改其显示和控制的部分。
实现模型自由旋转的数学基础
我们用鼠标实现模型的旋转,就好像手握一个包含模型的虚拟球一样。按一下鼠标,即在这个虚拟球上确定了一点,而拖动鼠标就是移动那个点,这样就实现了对虚拟球的旋转,同时达到旋转模型的目的。
这个虚拟球的中心位于显示屏的中心,这样球的一半则位于显示屏以外(外半球,如图1所示)。我们用鼠标点击的点将定义为外半球上的点。这种映射关系的数学定义为:
其中(x,y)是以球心为原点的屏幕坐标,R为球的半径。
接下来要做的就是在球上给定两个点后(起始点和终点)怎样确定旋转的轴和角度。从图2中可以看出:旋转轴是两个鼠标矢量(m1和m2)所张成的平面的法向量,所以可以通过求m1和m2的叉乘得到,即:
而旋转角度就是m1和m2之间的夹角a,因此:
在实际应用中,我们更习惯取a的两倍值进行旋转。因为这样将更有效地旋转模型。如果用鼠标点击视图的左中边缘,然后拖动至视图的右中边缘,则可实现模型以y轴为旋转轴的360度旋转。
从图3可以看出:在旋转的过程中,两个弧(R1和R2)的合成所形成的旋转弧等于R1的起始点和R2的终点形成的旋转弧。即意味着我们定义的虚拟球的旋转运动只决定于起始点和终点。
关键词 OpenGL 3DS文件格式 VC++ 自由旋转
现在已经有很多论文和书籍提到在OpenGL中实现读入和显示3DS文件中的模型。但是在很多场合,仅读入和显示是不够的。我们需要从各个角度观察模 型,以便更好地理解模型的形态,形成更为直观的感性认识。例如,在医学髁上骨折诊断中,如果把骨折后,断骨错位旋转的情况用三维模型模拟出来,并仅用鼠标 的拖动就能实现从任何角度观看骨折的情况,这将对医生做出正确的诊断大有裨益。这也是我们为何考虑实现此项功能的初衷。本文将简要介绍3DS文件格式,怎 样读入和显示模型,而重点放在通过鼠标拖动实现模型自由旋转的数学基础和编程实现的方法和经验。
3DS文件的格式以及读入和显示文件中模型的一些经验.
3DS文件是由许多块(chunk)组成的(大块中镶嵌子块)。由于至今为止,没有一个官方的文献说明其格式,所以还有很多未知的块。不过这并不影响我 们读入3DS文件中的模型。因为我们在读入时,可以根据自己的需要选择性地读入自己需要的块,而忽略掉那些不感兴趣或未知的块。这正是块结构给我们带来的 好处。
一个块由块信息和块数据组成。块信息又由块的ID(两个字节长的标识,如4D4D)和块的长度(四个字节,其实也就是下一个块 的偏移字节数)组成。用VC++以十六进制方式打开一3DS文件可以很清楚的看到其结构。在读入这种块结构(大块中嵌套小块,而块的结构固定)的文件时, 完全可以用递归的方法实现,而返回上一级(子块读完,返回父块)的条件则是当前已经读入的块的字节数是否等于块的长度。从父块转向读入其子块,则可用 switch语句实现,通过子块的ID判断进入哪个分支。
由于在网上有很多现成的这类程序,所以完全可是找一个类封装的比较好的程序,将其移植到自己的工程中就行了。当然需要做一些小小的改动,比如根据自己的需要修改其显示和控制的部分。
实现模型自由旋转的数学基础
我们用鼠标实现模型的旋转,就好像手握一个包含模型的虚拟球一样。按一下鼠标,即在这个虚拟球上确定了一点,而拖动鼠标就是移动那个点,这样就实现了对虚拟球的旋转,同时达到旋转模型的目的。
![]() |
这个虚拟球的中心位于显示屏的中心,这样球的一半则位于显示屏以外(外半球,如图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的终点形成的旋转弧。即意味着我们定义的虚拟球的旋转运动只决定于起始点和终点。
![](http://www.yesky.com/image20010518/180205.jpg)
编程实现自由旋转的方法和经验
1、首先建立一个虚拟球类
用面向对象的方法来解决问题,能使解决方案有很好的可移植性和可维护性。而VC++是功能强大的面向对象编程的工具,所以我们使用VC++面向对象程序设计的方案来实现自由旋转功能。虚拟球类的声明如下:
2、把鼠标坐标映射为虚拟球上的坐标
通过虚拟球的旋转来达到旋转模型的目的,关键在于把视图中鼠标点击和拖动的坐标映射为虚拟球上的坐标。
为此,我们首先简单的把鼠标点击和拖动的范围[0~width),[0~height)映射到 [-1~1],[1~ -1](在映射中我们颠倒了y坐标的符号,不然OpenGL中得不到正确的结果)。这样做可以使数学计算变得简单些,其映射如下:
其次,计算鼠标矢量,将鼠标坐标映射到虚拟球上,可以根据式1的定义完成这一步工作。
3、些相关变量的设定
为实现旋转我们还需要一些变量:
其中Transform是我们的最终变换结果,LastRot是上一次鼠标拖动得到的旋转结果,而ThisRot是当前鼠标拖动的结果。它们都被初始化为单位矩阵。
当我们点击鼠标时,我们从单位旋转矩阵开始旋转。当拖动鼠标时,我们计算从初始点到拖动点的旋转。尽管我们用这信息旋转屏幕上的模型,但值得注意的是我 们并不是真的旋转虚拟球自身。所以要得到累积的旋转结果,我们必须自己想办法,这也就是引入LastRot的原因。如果不累积旋转,模型就会在我们点击鼠 标时突然跑回到原始的状态。例如,如果关于X轴旋转90度后再旋转45度,希望得到135度的结果,但实际上得到的是45度。在下一次点击鼠标时,又会回 到原始的0度状态。
其他的变量,我们要做的就是在适当的时间和地点更新它们。虚拟球需要在窗口大小改变时重新设置它的边界; MousePt在鼠标点击和拖动时得到更新;isClicked和isRClicked分别标识鼠标的左键和右键是否按下,isClicked用来判断是 否处于按下和拖动状态,我们用isRClicked来重置所有的旋转,使其回到单位矩阵状态。
4、更新旋转矩阵
有了以上变量的更新,接下来就是根据这些更新,实现旋转矩阵的更新:
其中将四元数转化为旋转矩阵的函数为:
最后,把变换的结果应用于从3DS文件中读入的模型:
旋转的结果和问题分析
自由旋转的效果如图4所示。这种虚拟球旋转3DS文件中模型的方法操作简单方便而实用,达到预期的目的,但这种方法还有值得改进的地方。这个虚拟球的中 心是相对固定的(总在窗口的中心),如果模型的中心偏离虚拟球中心太远,旋转的效果就不是很好。最简单的解决办法是:用3DS MAX导出3DS文件前, 把模型的中心移到坐标原点。这是一个治标的办法,但适用且简单。而治本的方法就比较麻烦了,可以通过计算模型的中心来确定虚拟球的中心,使两个中心重合。 如果是多个模型,还应考虑实现鼠标捕获模型的功能,根据所选模型调节虚拟球的中心。
结束语
本文着重阐述了实现3DS文件中的模型自由旋转的数学基础和编程实现的过程。这项工作是计算机辅助诊断髁上骨折项目的一个重要组成部分。它的实现有利于 医生从各个角度观察骨折的模拟情况,形成较为直观的感性认识。对其它文件格式中的模型或辅助库中的模型都可以用此办法来实现自由旋转,所以具有较强的可移 植性和适用价值。
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(); |
旋转的结果和问题分析
自由旋转的效果如图4所示。这种虚拟球旋转3DS文件中模型的方法操作简单方便而实用,达到预期的目的,但这种方法还有值得改进的地方。这个虚拟球的中 心是相对固定的(总在窗口的中心),如果模型的中心偏离虚拟球中心太远,旋转的效果就不是很好。最简单的解决办法是:用3DS MAX导出3DS文件前, 把模型的中心移到坐标原点。这是一个治标的办法,但适用且简单。而治本的方法就比较麻烦了,可以通过计算模型的中心来确定虚拟球的中心,使两个中心重合。 如果是多个模型,还应考虑实现鼠标捕获模型的功能,根据所选模型调节虚拟球的中心。
![]() |
结束语
本文着重阐述了实现3DS文件中的模型自由旋转的数学基础和编程实现的过程。这项工作是计算机辅助诊断髁上骨折项目的一个重要组成部分。它的实现有利于 医生从各个角度观察骨折的模拟情况,形成较为直观的感性认识。对其它文件格式中的模型或辅助库中的模型都可以用此办法来实现自由旋转,所以具有较强的可移 植性和适用价值。