OpenGL NDC 左手还是右手?

Stella981
• 阅读 679

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

再谈 OpenGL 坐标变换(Coordinate Transformation)

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

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

OpenGL NDC 左手还是右手?

现代 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) 是坐标系原点。而摄像机坐标空间中,摄像机所在位置则是坐标系的原点位置。

OpenGL NDC 左手还是右手?

视口变换(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 。在右边,修改映射参数后,绿色三角形就挡住了红色三角形。

OpenGL NDC 左手还是右手?

最后

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

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


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

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这