摘要 簡述如何在OpenGL中, 讀入和顯示3DS檔案中的模型,並著重闡述通過滑鼠拖動對其進行自由旋轉的數學基礎和編程實現的方法。
關鍵詞 OpenGL 3DS檔案格式 VC++ 自由旋轉
現在已經有很多論文和書籍提到在OpenGL中實現讀入和顯示3DS檔案中的模型。但是在很多場合,僅讀入和顯示是不夠的。我們需要從各個角度觀察模 型,以便更好地理解模型的形態,形成更為直觀的感性認識。例如,在醫學髁上骨折診斷中,如果把骨折後,斷骨錯位旋轉的情況用3D 模型類比出來,並僅用滑鼠 的拖動就能實現從任何角度觀看骨折的情況,這將對醫生做出正確的診斷大有裨益。這也是我們為何考慮實現此項功能的初衷。本文將簡要介紹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的終點形成的旋轉弧。即意味著我們定義的虛擬球的旋轉運動只決定於起始點和終點。
編程實現自由旋轉的方法和經驗
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檔案中的模型自由旋轉的數學基礎和編程實現的過程。這項工作是電腦輔助診斷髁上骨折項目的一個重要組成部分。它的實現有利於 醫生從各個角度觀察骨折的類比情況,形成較為直觀的感性認識。對其它檔案格式中的模型或輔助庫中的模型都可以用此辦法來實現自由旋轉,所以具有較強的可移 植性和適用價值。