跳轉到內容

OpenGL 程式設計/物件選擇

來自 Wikibooks,開放世界中的開放書籍
使用滑鼠進行物件選擇

對於某些應用程式,能夠用滑鼠點選來選擇螢幕上的物件非常重要。如果您有一個具有非平凡投影(如透視投影)的複雜 3D 場景,那麼僅根據滑鼠指標的 x 和 y 座標很難確定您點選了哪個物件。幸運的是,OpenGL 具有一些功能可以使這變得容易得多。我們將研究兩種有價值的技術;第一個是根據滑鼠座標和深度緩衝區資訊找出我們物件空間中的座標,第二個是使用模板緩衝區對每個畫素進行唯一標記,以便我們可以確定它屬於哪個物件。

從幀緩衝區讀取資訊

[編輯 | 編輯原始碼]

對於這兩種技術,我們都需要在繪製完 3D 場景後從幀緩衝區讀取資訊。我們不需要讀取整個幀緩衝區,我們只需要知道我們用滑鼠點選的畫素是什麼。我們可以使用 GLUT 註冊一個回撥函式,以便在您點選時獲取滑鼠位置,並使用 glReadPixels() 函式找出該畫素是什麼。要註冊回撥函式,請使用

glutMouseFunc(onMouse);

回撥函式如下所示

void onMouse(int button, int state, int x, int y) {
  if(state != GLUT_DOWN)
    return;

  window_width = glutGet(GLUT_WINDOW_WIDTH);
  window_height = glutGet(GLUT_WINDOW_HEIGHT);

  GLbyte color[4];
  GLfloat depth;
  GLuint index;
  
  glReadPixels(x, window_height - y - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color);
  glReadPixels(x, window_height - y - 1, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
  glReadPixels(x, window_height - y - 1, 1, 1, GL_STENCIL_INDEX, GL_UNSIGNED_INT, &index);

  printf("Clicked on pixel %d, %d, color %02hhx%02hhx%02hhx%02hhx, depth %f, stencil index %u\n",
         x, y, color[0], color[1], color[2], color[3], depth, index);
}

glReadPixels() 函式非常簡單。前兩個引數是畫素的 x 和 y 偏移量,但 OpenGL 的 y 座標與 GLUT 相反。第三個和第四個引數是我們感興趣區域的寬度和高度。由於我們只需要一個畫素,因此我們指定了一個 1×1 的區域。接下來是我們要讀取的幀緩衝區的哪個元件。GL_RGBA 讀取完整的顏色資訊,GL_DEPTH_COMPONENT 讀取深度緩衝區的值,GL_STENCIL_INDEX 讀取模板緩衝區的值。第六個引數是我們想要以什麼格式儲存資料。請注意,對於 OpenGL ES 2.0,您可能只能選擇一些格式,具體取決於顯示卡的硬體和驅動程式功能。最後一個是指向我們想要將資料儲存到的變數或陣列的指標。

當然,只有在啟用深度和模板緩衝區時,您才能獲得合理的深度或模板資訊。您獲得的顏色和模板索引值是您所期望的。但是,深度值可能難以解釋,儘管您會清楚地看到,對於更靠近攝像機的物體,深度值更小。深度值始終介於 0 到 1 之間,預設情況下,背景的深度值為 1。

練習

反投影視窗座標

[編輯 | 編輯原始碼]

滑鼠指標的 x 和 y 座標以及深度緩衝區 z 值位於所謂的視窗座標中,但大多無用。我們想要做的是將它們轉換回物件空間座標,這是我們用來在其中指定頂點座標的座標系。要做到這一點,我們需要對視窗座標應用變換矩陣的逆矩陣。GLM 庫為我們提供了一個方便的函式,可以精確地執行我們想要的操作:glm::unProject()。以下是使用方式,假設您使用用於顯示場景的檢視和投影矩陣

  glm::vec4 viewport = glm::vec4(0, 0, window_width, window_height);
  glm::vec3 wincoord = glm::vec3(x, window_height - y - 1, depth);
  glm::vec3 objcoord = glm::unProject(wincoord, view, projection, viewport);

  printf("Coordinates in object space: %f, %f, %f\n",
         objcoord.x, objcoord.y, objcoord.z);

請注意,如果深度值為 1,則座標將毫無意義,因此您應該檢查一下。現在我們知道物件空間座標了,我們可以嘗試找出哪個物件最靠近這些座標。一個簡單的技術是迴圈遍歷所有物件,並檢查每個物件的中心到這些座標的距離。但這可能不會給出精確的匹配,尤其是在物件具有複雜形狀的情況下。如果物件位於規則網格上,您可以輕鬆地將物件座標轉換為網格座標。如果您使用八叉樹或 BSP 來儲存您的幾何圖形,則可以遍歷這些資料結構以快速找到您點選的位置。但是,如果您想準確地知道您點選的畫素上的哪個物件,並且上述方法不夠好,那麼您可以嘗試在下一節中介紹的模板緩衝區技術。

練習

  • 您通常將模型-檢視-投影矩陣應用於您繪製的物件。為什麼要在 unProjection 中不包括模型矩陣?
  • 假設您編寫了一個使用等軸測投影的 2D 遊戲,並且您可以從下到上繪製螢幕而無需使用深度緩衝區。您仍然可以僅使用 x 和 y 座標使用 glm::unProject() 嗎?
  • 更改紋理立方體教程以渲染至少 10 個較小的立方體。當點選視窗時,找出哪個立方體的中心最靠近您點選的點。

使用模板緩衝區識別物件

[編輯 | 編輯原始碼]

如果您沒有將模板緩衝區用於其他任何用途,則可以使用它來儲存有關螢幕上物件的的資訊。類似於我們在顏色緩衝區中繪製顏色,我們可以在模板緩衝區中繪製數字。首先,確保在對 glutInitDisplayMode() 的呼叫中添加了 GLUT_STENCIL。然後,假設我們要繪製十個物件,我們可以透過這種方式將每個物件的編號繪製到模板緩衝區

void onDisplay() {
  glClearColor(...);
  glClearStencil(0); // this is the default value
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);

  /* Any other initialization goes here */
  ...

  /* Enable stencil operations */
  glEnable(GL_STENCIL_TEST);
  glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

  for(int i = 0; i < 10; i++) {
    glStencilFunc(GL_ALWAYS, i + 1, -1);
    draw_object(i);
  }
}

首先,我們清除整個幀緩衝區,並確保模板緩衝區僅包含零。接下來,我們需要啟用模板測試,否則不會發生任何事情。我們使用 glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) 來確保無論何時寫入顏色緩衝區(特別是在深度測試成功時),都會寫入模板緩衝區,並且我們將用一個固定的新值替換現有值。然後,在繪製物件本身之前,我們將模板函式設定為始終透過模板測試,並將參考值設定為 i + 1(因為 0 已用於背景)。我們將掩碼設定為所有位都啟用(您也可以使用 ~00xff 而不是 -1,如果您願意)。從模板緩衝區讀取資訊時,如果您讀取零,則表示您點選了背景,否則表示您點選了某個物件。不要忘記在必要時減去 1。

練習

  • 修改紋理立方體示例以寫入模板緩衝區。使其能夠在您點選立方體時突出顯示它們。

更多技術

[編輯 | 編輯原始碼]

以上兩種技術簡單快速,但可能對您來說不夠好。儘管模板緩衝區技術是最準確的,但幾乎所有顯示卡都只支援 8 位模板緩衝區。這意味著您最多隻能識別 255 個物件。如果您需要超過 255 個物件,或者已經將模板緩衝區用於其他用途,請考慮使用以下替代方案之一

  • 組合來自模板緩衝區、顏色緩衝區和物件座標的資訊可能會為您提供一個獨特的解決方案。
  • 使用 glScissor() 多次繪製場景,以僅渲染您感興趣的畫素。在每次傳遞中,您可以從模板緩衝區獲取 8 位資訊,因此 3 次傳遞允許您唯一地識別 1600 萬個物件。
  • 再次繪製場景,但不要使用模板緩衝區,而是為每個物件賦予一個唯一的純色。這將為您提供每個畫素的 24 位甚至 32 位數字。確保您停用了抗鋸齒。

< OpenGL 程式設計

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