跳轉到內容

OpenGL 程式設計/Glescraft 4

來自華夏公益教科書
一個體素世界。

有多種方法可以操縱 3D 場景的視角。對於計算機輔助設計 (CAD) 軟體,您通常使用固定相機點,並使用 (3D) 滑鼠或軌跡球來旋轉或移動感興趣的物件。在許多遊戲中,尤其是第一人稱射擊遊戲中,世界是靜止的,但您可以移動相機在世界中移動。顯示卡沒有區分這兩種方法,它只是應用您提供的模型-檢視-投影矩陣。主要區別在於我們使用滑鼠和/或鍵盤操縱 MVP 矩陣的方式。對於我們的第一人稱相機,我們從兩個向量派生 MVP 矩陣;相機位置和視角。

glm::vec3 position;
glm::vec2 angles;

在 GLUT 中捕獲滑鼠移動

[編輯 | 編輯原始碼]

GLUT 有兩個回撥函式來捕獲滑鼠移動。一個是在至少按下了一個滑鼠按鈕時移動滑鼠時呼叫,另一個是在沒有按下任何滑鼠按鈕時呼叫。在我們的例子中,我們不想區分這兩種情況,因此我們可以為這兩個回撥函式註冊相同的函式

glutMotionFunc(motion);
glutPassiveMotionFunc(motion);

當您的應用程式或遊戲處於第一人稱模式時,您通常不想看到滑鼠游標。我們可以透過這種方式停用它

glutSetCursor(GLUT_CURSOR_NONE);

如果您不再處於第一人稱模式,例如當您顯示選單或遊戲暫停時,您可以使用以下方法再次顯示預設滑鼠游標glutSetCursor(GLUT_CURSOR_INHERIT).

這個motion()回撥函式將獲取相對於視窗左上角的當前滑鼠座標(如果視窗處於全屏模式,則相對於螢幕左上角)。但是,我們不想知道當前座標,我們只想知道滑鼠移動了多少。當然,我們可以從之前的呼叫中減去座標motion()從當前座標中減去,但這在滑鼠游標位於視窗或螢幕邊緣時不再起作用!GLUT 中的解決方案是,每當滑鼠移動時,使用以下方法將滑鼠游標移回視窗中心glutWarpPointer()函式。但是,使用此函式會導致 GLUT 再次呼叫運動回撥函式,因此我們必須忽略每次對motion():

void motion(int x, int y) {
  static bool wrap = false;

  if(!wrap) {
    int ww = glutGet(GLUT_WINDOW_WIDTH);
    int wh = glutGet(GLUT_WINDOW_HEIGHT);

    int dx = x - ww / 2;
    int dy = y - wh / 2;

    // Do something with dx and dy here

    // move mouse pointer back to the center of the window
    wrap = true;
    glutWarpPointer(ww / 2, wh / 2);
  } else {
    wrap = false;
  }
}

在上面的函式中,dxdy變數儲存滑鼠游標移動的畫素距離。

觀察方向

[編輯 | 編輯原始碼]

作為生活在行星上的地球人,我們在水平面上的環顧四周與上下看之間有著很大的區別。部分原因是我們自己所處的與地面相同的水平面上發生了最有趣的事情,但也因為我們可以隨意圍繞我們的(垂直)軸旋轉,但我們只能將頭向上和向下旋轉一定程度。在 FPS 遊戲中,您可以隨意圍繞垂直軸旋轉,但向上和向下的旋轉限制在 +/- 90 度。除了這種限制之外,視角的變化等於滑鼠移動量乘以一個縮放係數

    const float mousespeed = 0.001;

    angles.x += dx * mousespeed;
    angles.y += dy * mousespeed;

    if(angles.x < -M_PI)
      angles.x += M_PI * 2;
    else if(angles.x > M_PI)
      angles.x -= M_PI * 2;

    if(angles.y < -M_PI / 2)
      angles.y = -M_PI / 2;
    if(angles.y > M_PI / 2)
      angles.y = M_PI / 2;

angles向量,我們可以使用簡單的三角函式計算我們正在觀察的方向

glm::vec3 lookat;
lookat.x = sinf(angles.x) * cosf(angles.y);
lookat.y = sinf(angles.y);
lookat.z = cosf(angles.x) * cosf(angles.y);

給定一個位置向量和lookat向量,我們可以建立如下檢視矩陣

glm::vec3 position;
glm::mat4 view = glm::lookAt(position, position + lookat, glm::vec3(0, 1, 0));

練習

  • 許多遊戲將向上和向下的觀看限制在略小於 90 度。你能想到這樣做的原因嗎?
  • 如果將向上/向下角度超過 90 度會發生什麼?場景會上下顛倒嗎?
  • 而不是計算lookat向量並使用glm::lookAt()函式,嘗試構建view使用矩陣glm::rotate()。這會產生相同的結果嗎?如果將向上/向下角度超過 90 度也是如此嗎?

相機移動

[編輯 | 編輯原始碼]

由於我們已經使用滑鼠來確定視角,因此只剩下鍵盤來移動相機位置。雖然計算機可以告訴您移動了滑鼠多遠,但按鍵要麼被按下,要麼沒有被按下。雖然我們可以輕鬆地註冊一個鍵盤迴調函式,每次按下按鍵時都會將相機位置移動固定量,但這會導致相機移動非常卡頓。我們不想直接控制位置,而是想使用鍵盤改變速度。如果沒有按下任何按鍵,我們的速度為零。如果按下向上箭頭鍵,我們將在向前方向上具有一定的速度。如果按下向下箭頭鍵,我們將在向前方向上具有負速度。如果按下向右箭頭,我們在側向方向上具有一定的速度,等等。我們可以使用以下方法註冊一個回撥函式glutSpecialFunc()每次按下“特殊”鍵(如游標鍵)時都會呼叫該函式。但是,我們還需要知道該鍵何時再次釋放。為此,我們可以使用以下方法註冊一個回撥函式glutSpecialUpFunc():

glutSpecialFunc(special);
glutSpecialUpFunc(specialup);

這兩個回撥函式如下所示

const int left = 1; 
const int right = 2;
const int forward = 4; 
const int backward = 8; 
const int up = 16; 
const int down = 32;

int move = 0;

void special(int key, int x, int y) {
  if(key == KEY_LEFT)
    move |= left;     
  if(key == KEY_RIGHT)
    move |= right;
  if(key == KEY_UP)
    move |= forward;     
  if(key == KEY_DOWN)
    move |= backward;     
  if(key == KEY_PAGEUP)
    move |= up;     
  if(key == KEY_PAGEDOWN)
    move |= down;
}

void specialup(int key, int x, int y) {
  if(key == KEY_LEFT)
    move &= ~left;     
  if(key == KEY_RIGHT)
    move &= ~right;
  if(key == KEY_UP)
    move &= ~forward;     
  if(key == KEY_DOWN)
    move &= ~backward;     
  if(key == KEY_PAGEUP)
    move &= ~up;     
  if(key == KEY_PAGEDOWN)
    move &= ~down;
}

這個move變數將包含當前按下的移動鍵的位掩碼。應定期更新相機位置,最好每幀更新一次。我們可以在 GLUT 中註冊一個函式,該函式在它什麼都不用做時被呼叫

glutIdle(idle);

然後,我們可以在該函式中更新相機位置向量,並告訴 GLUT 重新繪製場景。為了更新相機位置向量,我們需要知道“向前”、“向右”等方向。在大多數遊戲中,“向前”是我們正在觀察的方向,但只在水平面上。通常,這是因為您站在地面上,即使您在向上或向下看時走動,您也會停留在地板上。從“向前”向量,我們還可以推匯出“向右”向量,“向上”向量 simply 指向正 y 方向。結果如下

void idle() {
  static int pt = 0;
  const float movespeed = 10;

  // Calculate time since last call to idle()
  int t = glutGet(GLUT_ELAPSED_TIME);
  float dt = (t - pt) * 1.0e-3;
  pt = t;
  
  // Calculate movement vectors
  glm::vec3 forward_dir = vec3(sinf(angles.x), 0, cosf(angles.x));
  glm::vec3 right_dir = vec3(-forward_dir.z, 0, forward_dir.x);

  // Update camera position
  if(move & left)
    position -= right_dir * movespeed * dt;
  if(move & right)
    position += right_dir * movespeed * dt;
  if(move & forward)
    position += forward_dir * movespeed * dt;
  if(move & backward)
    position -= forward_dir * movespeed * dt;
  if(move & up)
    position.y += movespeed * dt;
  if(move & down)
    position.y -= movespeed * dt;

  // Redraw the scene
  glutPostRedisplay();
}

練習

  • 不要直接使用三角函式,嘗試推匯出forward_dirright_dir來自lookat和“向上”向量使用向量代數。

< OpenGL 程式設計

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