之前看到一篇文章說默認情況下 OpenGL NDC 是基于左手坐標系。我在我之前的博文中也提到過。之后我在想為什么 OpenGL NDC 是基于左手坐標系?真的是基于左手坐標系嗎?所以這篇博客就是來找到答案。
再談 OpenGL 坐標變換(Coordinate Transformation)
紅寶書中第五章很清楚的描述了坐標變換,這里的圖示也是引用那里,具體內容可以查看紅寶書,我在這里僅簡單提一下。
在坐標系中指定坐標點,將坐標點連接起來繪制幾何形狀。繪制復雜的場景時,不同的物體,靜態(tài)的或是動態(tài)的,必然涉及到坐標變換。如下圖,用戶指定物體的坐標,然后在 user transform 階段變換物體,之后就由 OpenGl 進行 clipping 和 viewport transform 操作,然后到了 Opengl 管線中光柵化階段,最終由片段著色器(fragment shader)輸出片段顏色,顯示在屏幕上。

現(xiàn)代 OpenGL 特地有給用戶預留 user transform 這一些階段。在這一階段用戶自己根據(jù)需求進行 scale 、rotate 、translate 、project (縮放,旋轉,平移,投影)等操作。如下圖在 user transform 中就可以看見我們熟悉的 MVP 變換。MVP 變換分別對應著 model matrix 、view matrix 、projection matrix (model 矩陣,view 矩陣,投影矩陣)。假設只用到頂點著色器(vertex shader)和片段著色器(fragment shader)。在頂點著色器中為 gl_Position 賦值后,user transform 就結束了,接下來 OpenGL 根據(jù)輸入的頂點,最終在屏幕上繪制出形狀。
其中通常用 model matrix 把物體變換到 world coordinate space (世界坐標空間)。通常用 view matrix 再世界坐標空間中的物體變換到 eye/camera coordinate space (眼睛坐標空間或攝像機坐標空間)中。物體間的交互通常在世界坐標空間中計算,這是所有物體共用的坐標空間。而有時會根據(jù)需要在攝像機坐標空間中進行計算,比如光照計算。在世界坐標空間中 ,(0, 0, 0) 是坐標系原點。而攝像機坐標空間中,攝像機所在位置則是坐標系的原點位置。

視口變換(Viewport Transformation)
如上圖,頂點坐標經(jīng)過 perspective division(OpenGL divide by w)后,要在標準化設備坐標空間(Normalized Device Coordinates,NDC)中對頂點進行剪裁(clipping)。NDC 坐標空間范圍在 X 軸,Y 軸和 Z 軸都是 [-1, 1] 。處于 NDC 范圍外的頂點會被忽略掉。經(jīng)過剪裁,余下的頂點是如何顯示在窗口上的呢?這時就涉及到了視口變換。glViewport 和 glDepthRange 用于控制視口變換。glViewport 用于將 NDC 中的 x 和 y 坐標變換到窗口坐標空間中(window coordinate)。窗口坐標 x 表示水平方向的像素,y 表示豎直方向的像素。而 glDepthRange 則把 NDC 的 z 坐標映射到窗口深度坐標(window depth)。窗口深度坐標的范圍是 [0, 1] 。
需要調用 glEnable 啟用 GL_DEPTH_TEST 深度測試,glDepthRange 設置才會起作用,畢竟默認的渲染順序是后面的繪制的像素覆蓋前面的繪制的像素。開啟深度測試后,不同深度值顯示的先后順序則是通過 glDepthFunc 設置。
介紹了坐標變換和視口變換后后,你應該對于物體從你指定坐標到最終顯示在屏幕上經(jīng)歷了哪些變換,有了初步的認識。下面就通過問題和小例子來感受下。
問題,NDC Z 軸范圍真的是 [-1, 1] ?
藍寶書和紅寶書都提到 NDC Z 軸范圍是從 [0, 1] ,但是其他資料文章都是寫著 [-1, 1] 。不知道為什么藍寶書和紅寶書出了這么多版本后未修改,應該是在的角度不同吧。先不管這些文字上給的范圍,經(jīng)過上面的介紹,我們完全能計算出 Z 軸的實際范圍,于是來在代碼中驗證吧。
代碼中忽略了 MV 變換,僅僅進行投影變換,并且在透視投影和正交投影中分別計算 Z 軸范圍。要計算 Z 軸范圍,就是在近平面(near plane)和遠平面(far plane)各取一點,進行投影變換和透視除法,這時得到的坐標的 z 值就是實際 Z 軸的范圍。
注意,默認情況下 glm::perspective 和 glm::ortho 函數(shù)中指定的近平面和遠平面距離是基于右手坐標系來實現(xiàn)的,并且距離是到原點 (0, 0, 0) 的距離(glm::perspective 有左手坐標系實現(xiàn))。我們的代碼中采用的是默認方式。具體代碼如下。
void
test_perspective_proj() {
float neardist = 0.1f, fardist = 100.0f;
glm::vec4 nearpoint(0.0f, 0.0f, -neardist, 1.0f);
glm::vec4 farpoint(0.0f, 0.0f, -fardist, 1.0f);
glm::mat4 persmat = glm::perspective(glm::radians(45.0f), 4.0f/3.0f, neardist, fardist);
glm::vec4 nearpoint_clip = persmat * nearpoint;
glm::vec4 farpoint_clip = persmat * farpoint;
nearpoint_clip /= nearpoint_clip.w;
farpoint_clip /= farpoint_clip.w;
glm::vec3 ndc_near(nearpoint_clip);
glm::vec3 ndc_far(farpoint_clip);
printf("one point in near plane:<%f, %f, %f>\n", ndc_near.x, ndc_near.y, ndc_near.z);
printf("one point in far plane:<%f, %f, %f>\n", ndc_far.x, ndc_far.y, ndc_far.z);
}
void
test_orthographic_proj() {
float neardist = -100.0f, fardist = 100.0f;
glm::vec4 nearpoint(0.0f, 0.0f, -neardist, 1.0f);
glm::vec4 farpoint(0.0f, 0.0f, -fardist, 1.0f);
glm::mat4 persmat = glm::ortho(-4.0f, 4.0f, -3.0f, 3.0f, neardist, fardist);
glm::vec4 nearpoint_clip = persmat * nearpoint;
glm::vec4 farpoint_clip = persmat * farpoint;
nearpoint_clip /= nearpoint_clip.w;
farpoint_clip /= farpoint_clip.w;
glm::vec3 ndc_near(nearpoint_clip);
glm::vec3 ndc_far(farpoint_clip);
printf("one point in near plane:<%f, %f, %f>\n", ndc_near.x, ndc_near.y, ndc_near.z);
printf("one point in far plane:<%f, %f, %f>\n", ndc_far.x, ndc_far.y, ndc_far.z);
}
輸出分別如下:
one point in near plane:<0.000000, 0.000000, -1.000000>
one point in far plane:<0.000000, 0.000000, 1.000000>
one point in near plane:<0.000000, 0.000000, -1.000000>
one point in far plane:<0.000000, 0.000000, 1.000000>
由此可知 NDC Z 軸范圍確實是 [-1, 1] 。
問題,NDC 是左手坐標系還是右手坐標系?
其實講到這里,相信你已感覺到?jīng)]有絕對的左手還是右手坐標系。當不作任何設置時,NDC 是所謂的左手坐標系,即 z 坐標越小顯示越靠前。然后通過 glDepthRange 和 glDepthFunc 設置后完全可以產(chǎn)生相反的結果。下面的例子就會說明這一點。
glEnable(GL_DEPTH_TEST);
glUseProgram(_program);
glBindVertexArray(_vao);
// 繪制左邊的兩個三角形
glDepthRangef(0.0f, 1.0f);
glUniform1f(_xoffset_loc, -0.5f);
glDrawArrays(GL_TRIANGLES, 0, 6);
// 調節(jié)映射參數(shù)后,再在右邊繪制三角形
glDepthRangef(1.0f, 0.0f);
glUniform1f(_xoffset_loc, 0.5f);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glUseProgram(0);
運行截圖如下。紅色三角形的 z 坐標是 0.0f ,而綠色三角形的 z 坐標是 0.5f 。在右邊,修改映射參數(shù)后,綠色三角形就擋住了紅色三角形。

最后
完整的示例代碼在 blogsnippet/opengl/ndc 中。
其實 OpenGL 提供了靈活的方式處理 Z 軸。以上示例中 glDepthRange 和 glDepthFunc 是一方面。另外也可以在調用 glm::perspective 或 ortho 時設置 near 比 far 大,可以查看效果。因此理解了本質后,看文章或者代碼能更好的理解。