跳轉到內容

OpenGL 程式設計/現代 OpenGL 教程 虛擬軌跡球

來自華夏公益教科書,開放世界開放書籍

虛擬軌跡球是一個用滑鼠自然地旋轉物體的工具。

旋轉期間的虛擬軌跡球角度

想象一個虛擬球,它就在屏幕後面。用滑鼠點選它,你就會把它捏住,然後移動滑鼠,你就會讓球繞著它的中心旋轉。相同的旋轉也會應用到 OpenGL 場景中的一個物體上!

右邊的圖顯示了虛擬球的俯檢視。底部的黑線是螢幕,x1 和 x2 是拖動過程中兩個連續的滑鼠位置。這是在 2D 中顯示的,但相同的原理也適用於 3D。

目標是計算 α 角和旋轉軸。這是你理解數學一旦你開始認真對待 OpenGL 就非常必要的點。特別是,我們將需要勾股定理、向量點積和叉積。

我們將

  1. 將螢幕座標(以畫素為單位)轉換為相機座標(在 [-1, 1] 中)
  2. 計算向量 OP1 和 OP2,即與我們的滑鼠點選匹配的球體表面的點
    • x 和 y 座標直接取自相機座標中的點選
    • z 座標使用經典勾股定理計算
    • 如果 P1 或 P2 距離球體太遠 (),我們將對其進行歸一化以獲得球體表面上的最近點
  3. 我們有 ,球體的大小為 1 (),所以我們使用 獲取角度。
  4. 在 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;
  }
}

計算 OP1 和 OP2

[編輯 | 編輯原始碼]

我們新增一個新函式來計算軌跡球表面點。

/**
 * 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(這裡命名為 vavb),我們就可以使用 acos(dot(va,vb)) 計算角度。

由於我們使用的是 float 變數,因此可能會出現精度問題:dot 可能會返回一個略大於 1 的值,acos 將返回 nan,這意味著一個無效的浮點數。結果是我們的旋轉矩陣將全部亂套,通常我們的物體將從螢幕上消失!為了解決這個問題,我們用最大值 1.0 對該值進行限制。

另一個技巧是將旋轉軸從相機座標轉換為物體座標。當相機和物體放置不同時,這很有用。例如,如果你將物體繞 Y 軸旋轉 90°(“轉向”右邊),然後用滑鼠進行垂直移動,你會在相機 X 軸上進行旋轉,但對於物體來說應該變成繞 Z 軸旋轉(平面桶滾)。透過將軸轉換為物體座標,旋轉將尊重使用者在相機座標系中工作(所見即所得)。為了從相機座標系轉換為物體座標系,我們取 MV 矩陣(來自 MVP 矩陣三元組)的逆。

最後,我們可以像往常一樣使用 glm::rotate 應用變換 :)

  • 旋轉角度是否與滑鼠移動成正比?嘗試在虛擬球體的邊界附近移動滑鼠。
  • 當滑鼠距離太遠時,虛擬球體將停止滾動。其他滑鼠控制也是可能的。例如,研究如何在 Blender 3D 建模器中使用中間按鈕拖動。
  • 嘗試不同的滾動速度,方法是將旋轉角度相乘。

< OpenGL 程式設計

瀏覽並下載 完整程式碼
華夏公益教科書