OpenGL 程式設計/現代 OpenGL 教程 虛擬軌跡球
虛擬軌跡球是一個用滑鼠自然地旋轉物體的工具。

想象一個虛擬球,它就在屏幕後面。用滑鼠點選它,你就會把它捏住,然後移動滑鼠,你就會讓球繞著它的中心旋轉。相同的旋轉也會應用到 OpenGL 場景中的一個物體上!
右邊的圖顯示了虛擬球的俯檢視。底部的黑線是螢幕,x1 和 x2 是拖動過程中兩個連續的滑鼠位置。這是在 2D 中顯示的,但相同的原理也適用於 3D。
目標是計算 α 角和旋轉軸。這是你理解數學一旦你開始認真對待 OpenGL 就非常必要的點。特別是,我們將需要勾股定理、向量點積和叉積。
我們將
- 將螢幕座標(以畫素為單位)轉換為相機座標(在 [-1, 1] 中)
- 計算向量 OP1 和 OP2,即與我們的滑鼠點選匹配的球體表面的點
- x 和 y 座標直接取自相機座標中的點選
- z 座標使用經典勾股定理計算
- 如果 P1 或 P2 距離球體太遠 (),我們將對其進行歸一化以獲得球體表面上的最近點
- 我們有 ,球體的大小為 1 (),所以我們使用 獲取角度。
- 在 3D 中獲取旋轉軸,我們計算 ,這將提供一個垂直單位向量
GLUT 提供了一種獲取滑鼠點選和拖動事件的方法
glutMouseFunc(onMouse) 將為每次滑鼠點選呼叫 onMouse(int button, int state, int x, int y),其中
- button 是 GLUT_LEFT_BUTTON、GLUT_MIDDLE_BUTTON 或 GLUT_RIGHT_BUTTON
- state 是 GLUT_DOWN 或 GLUT_UP
- x 和 y 是螢幕座標,從左上角開始(y 與 OpenGL 座標相反!)
glutMotionFunc(onMotion) 將為每次按下任何按鈕的滑鼠移動呼叫 onMotion(int x, int y),其中 x 和 y 是螢幕座標。
您還有 glutPassiveMotionFunc(...),它在沒有按下任何按鈕的情況下以類似的方式處理滑鼠移動。
因此,我們添加了兩個函式來跟蹤按下左鍵時的滑鼠移動
/* Global */
int last_mx = 0, last_my = 0, cur_mx = 0, cur_my = 0;
int trackball_on = false;
/* main() */
glutMouseFunc(onMouse);
glutMotionFunc(onMotion);
void onMouse(int button, int state, int x, int y) {
if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
trackball_on = true;
last_mx = cur_mx = x;
last_my = cur_my = y;
} else {
trackball_on = false;
}
}
void onMotion(int x, int y) {
if (trackball_on) { // if left button is pressed
cur_mx = x;
cur_my = y;
}
}
我們新增一個新函式來計算軌跡球表面點。
/**
* Get a normalized vector from the center of the virtual ball O to a
* point P on the virtual ball surface, such that P is aligned on
* screen's (X,Y) coordinates. If (X,Y) is too far away from the
* sphere, return the nearest point on the virtual ball surface.
*/
glm::vec3 get_trackball_vector(int x, int y) {
glm::vec3 P = glm::vec3(1.0*x/screen_width*2 - 1.0,
1.0*y/screen_height*2 - 1.0,
0);
P.y = -P.y;
float OP_squared = P.x * P.x + P.y * P.y;
if (OP_squared <= 1*1)
P.z = sqrt(1*1 - OP_squared); // Pythagoras
else
P = glm::normalize(P); // nearest point
return P;
}
我們首先將 x,y 螢幕座標轉換為 [-1,1] 座標(並反轉 y 座標)。然後我們使用勾股定理來檢查 OP 向量的長度並計算 z 座標,如上所述。
/* onIdle() */
if (cur_mx != last_mx || cur_my != last_my) {
glm::vec3 va = get_trackball_vector(last_mx, last_my);
glm::vec3 vb = get_trackball_vector( cur_mx, cur_my);
float angle = acos(min(1.0f, glm::dot(va, vb)));
glm::vec3 axis_in_camera_coord = glm::cross(va, vb);
glm::mat3 camera2object = glm::inverse(glm::mat3(transforms[MODE_CAMERA]) * glm::mat3(mesh.object2world));
glm::vec3 axis_in_object_coord = camera2object * axis_in_camera_coord;
mesh.object2world = glm::rotate(mesh.object2world, glm::degrees(angle), axis_in_object_coord);
last_mx = cur_mx;
last_my = cur_my;
}
一旦我們有了 OP1 和 OP2(這裡命名為 va 和 vb),我們就可以使用 acos(dot(va,vb)) 計算角度。
由於我們使用的是 float 變數,因此可能會出現精度問題:dot 可能會返回一個略大於 1 的值,acos 將返回 nan,這意味著一個無效的浮點數。結果是我們的旋轉矩陣將全部亂套,通常我們的物體將從螢幕上消失!為了解決這個問題,我們用最大值 1.0 對該值進行限制。
另一個技巧是將旋轉軸從相機座標轉換為物體座標。當相機和物體放置不同時,這很有用。例如,如果你將物體繞 Y 軸旋轉 90°(“轉向”右邊),然後用滑鼠進行垂直移動,你會在相機 X 軸上進行旋轉,但對於物體來說應該變成繞 Z 軸旋轉(平面桶滾)。透過將軸轉換為物體座標,旋轉將尊重使用者在相機座標系中工作(所見即所得)。為了從相機座標系轉換為物體座標系,我們取 MV 矩陣(來自 MVP 矩陣三元組)的逆。
最後,我們可以像往常一樣使用 glm::rotate 應用變換 :)
- 旋轉角度是否與滑鼠移動成正比?嘗試在虛擬球體的邊界附近移動滑鼠。
- 當滑鼠距離太遠時,虛擬球體將停止滾動。其他滑鼠控制也是可能的。例如,研究如何在 Blender 3D 建模器中使用中間按鈕拖動。
- 嘗試不同的滾動速度,方法是將旋轉角度相乘。