OpenGL NDC 左手还是右手?

之前看到一篇文章说默认情况下 OpenGL NDC 是基于左手坐标系。我在我之前的博文中也提到过。之后我在想为什么 OpenGL NDC 是基于左手坐标系?真的是基于左手坐标系吗?所以这篇博客就是来找到答案。

再谈 OpenGL 坐标变换(Coordinate Transformation)

红宝书中第五章很清楚的描述了坐标变换,这里的图示也是引用那里,具体内容可以查看红宝书,我在这里仅简单提一下。

在坐标系中指定坐标点,将坐标点连接起来绘制几何形状。绘制复杂的场景时,不同的物体,静态的或是动态的,必然涉及到坐标变换。如下图,用户指定物体的坐标,然后在 user transform 阶段变换物体,之后就由 OpenGl 进行 clipping 和 viewport transform 操作,然后到了 Opengl 管线中光栅化阶段,最终由片段着色器(fragment shader)输出片段颜色,显示在屏幕上。

CoordinateSystem

现代 OpenGL 特地有给用户预留 user transform 这一些阶段。在这一阶段用户自己根据需求进行 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 根据输入的顶点,最终在屏幕上绘制出形状。

其中通常用 model matrix 把物体变换到 world coordinate space (世界坐标空间)。通常用 view matrix 再世界坐标空间中的物体变换到 eye/camera coordinate space (眼睛坐标空间或摄像机坐标空间)中。物体间的交互通常在世界坐标空间中计算,这是所有物体共用的坐标空间。而有时会根据需要在摄像机坐标空间中进行计算,比如光照计算。在世界坐标空间中 ,(0, 0, 0) 是坐标系原点。而摄像机坐标空间中,摄像机所在位置则是坐标系的原点位置。

User Transforms

视口变换(Viewport Transformation)

如上图,顶点坐标经过 perspective division(OpenGL divide by w)后,要在标准化设备坐标空间(Normalized Device Coordinates,NDC)中对顶点进行剪裁(clipping)。NDC 坐标空间范围在 X 轴,Y 轴和 Z 轴都是 [-1, 1] 。处于 NDC 范围外的顶点会被忽略掉。经过剪裁,余下的顶点是如何显示在窗口上的呢?这时就涉及到了视口变换。glViewportglDepthRange 用于控制视口变换。glViewport 用于将 NDC 中的 x 和 y 坐标变换到窗口坐标空间中(window coordinate)。窗口坐标 x 表示水平方向的像素,y 表示竖直方向的像素。而 glDepthRange 则把 NDC 的 z 坐标映射到窗口深度坐标(window depth)。窗口深度坐标的范围是 [0, 1] 。

需要调用 glEnable 启用 GL_DEPTH_TEST 深度测试,glDepthRange 设置才会起作用,毕竟默认的渲染顺序是后面的绘制的像素覆盖前面的绘制的像素。开启深度测试后,不同深度值显示的先后顺序则是通过 glDepthFunc 设置。

介绍了坐标变换和视口变换后后,你应该对于物体从你指定坐标到最终显示在屏幕上经历了哪些变换,有了初步的认识。下面就通过问题和小例子来感受下。

问题,NDC Z 轴范围真的是 [-1, 1] ?

蓝宝书和红宝书都提到 NDC Z 轴范围是从 [0, 1] ,但是其他资料文章都是写着 [-1, 1] 。不知道为什么蓝宝书和红宝书出了这么多版本后未修改,应该是在的角度不同吧。先不管这些文字上给的范围,经过上面的介绍,我们完全能计算出 Z 轴的实际范围,于是来在代码中验证吧。

代码中忽略了 MV 变换,仅仅进行投影变换,并且在透视投影和正交投影中分别计算 Z 轴范围。要计算 Z 轴范围,就是在近平面(near plane)和远平面(far plane)各取一点,进行投影变换和透视除法,这时得到的坐标的 z 值就是实际 Z 轴的范围。

注意,默认情况下 glm::perspectiveglm::ortho 函数中指定的近平面和远平面距离是基于右手坐标系来实现的,并且距离是到原点 (0, 0, 0) 的距离(glm::perspective 有左手坐标系实现)。我们的代码中采用的是默认方式。具体代码如下。

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 是左手坐标系还是右手坐标系?

其实讲到这里,相信你已感觉到没有绝对的左手还是右手坐标系。当不作任何设置时,NDC 是所谓的左手坐标系,即 z 坐标越小显示越靠前。然后通过 glDepthRangeglDepthFunc 设置后完全可以产生相反的结果。下面的例子就会说明这一点。

glEnable(GL_DEPTH_TEST);

glUseProgram(_program);
    glBindVertexArray(_vao);
        // 绘制左边的两个三角形
        glDepthRangef(0.0f, 1.0f);
        glUniform1f(_xoffset_loc, -0.5f);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        
        // 调节映射参数后,再在右边绘制三角形
        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 。在右边,修改映射参数后,绿色三角形就挡住了红色三角形。

RunNDCDepth

最后

完整的示例代码在 blogsnippet/opengl/ndc 中。

其实 OpenGL 提供了灵活的方式处理 Z 轴。以上示例中 glDepthRangeglDepthFunc 是一方面。另外也可以在调用 glm::perspectiveortho 时设置 near 比 far 大,可以查看效果。因此理解了本质后,看文章或者代码能更好的理解。


我的博客地址 https://my.oschina.net/iirecord/blog

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容