OpenGLES 透视变换与屏幕UV坐标
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenGLES 透视变换与屏幕UV坐标相关的知识,希望对你有一定的参考价值。
参考技术A 一、从需求说起本人在做3D贴纸的时候,遇到这样的一个需求,在3D贴纸需要和图像进行混合。做远小近大的3D效果,需要将二维的贴纸经过透视变换绘制到屏幕上,如果要添加混合效果,则必须知道变换后的坐标,如果坐标对不上,则会导致混合后的贴纸绘制到屏幕上可能会出现错乱、重叠的情况,因此如何计算重叠部分的准确坐标是实现混合的关键。为此,重新拾起已经被遗忘了的数学知识,将透视变换过程重新推导一遍,以便更好地理解透视变换以及如何转换到屏幕的空间坐标的。
二、透视变换推导
1、透视投影公式
下图给出了一个空间点(x, y, z)到一般的投影参考点(xv, yv, zv)的投影路径。该投影线与观察平面交于坐标位置(xp, yp, zvp), 其中zvp是观察平面上选择的位于z轴上的点。
由此我们可以计算得到坐标位置的参数方程如下:
当投影参考点在Z轴上时,xv = yv = 0,此时有:
因此,我们可以建立一个变换矩阵将一个空间位置转为齐次坐标位置,使得矩阵仅包含透视参数而不包含坐标值。然后观察坐标系的透视投影变换分两步实现。先用透视变换矩阵计算齐次坐标:
Ph 是齐次点(xh, yh, zh, h) 的列矩阵表示, 而P是坐标(x, y, z, 1)的列矩阵表示,在实际当中,透视矩阵需要跟观察矩阵(ViewMatrix)合并,然后将组合矩阵应用于场景的世界坐标描述以生成齐次坐标。
为了防止z轴除以齐次参数h后出现扭曲,我们需要通过为z变换设定矩阵元素从而对透视投影zp的坐标进行规范化。有多种方法选择矩阵元素。下面是一种可能形成透视投影矩阵的方法:
5、对称的视锥体
从投影参考点到裁剪窗口中心平穿过观察体的线就会说透视投影棱台的中心线。棱台中心线与观察平面相交于坐标(xv, yv, zvp)位置,用窗口尺寸表示裁剪窗口的对角位置,可得:
整体如下图所示:
可以求得裁剪窗口高度:
6、斜透视投影棱台
如果透视投影观察体中心线并不垂直于观察平面,则得到一个斜棱台(oblique frustum)。
为了方便计算,将投影参考点(xv, yv, zv) = (0, 0, 0),可得到错切变换矩阵的元素:
如果将观察平面放在近裁剪平面处,则透视投影矩阵可以进一步简化。将裁剪窗口中心移到观察平面坐标位置(0, 0)处,需要选择的错切参数值满足:
当投影参考点位于观察坐标原点切近裁剪平面与观察平面重合时,透视投影矩阵可以简化为:
将透视矩阵和错切矩阵综合,就可以得到下面的将场景坐标位置转换成齐次正交坐标的斜透视投影矩阵。该变换的投影参考点是观察坐标原点,而近裁剪平面是观察平面。
7、规范化透视投影变换坐标
矩阵将观察坐标系中的对角位置变换到透视投影齐次坐标。使用齐次参数h 除齐次坐标,可得实际的正交投影坐标。该透视投影将棱台观察体中所有点变换成矩形平行管道观察体中的位置,变换过程的最后一步是将该平行管道映射到规范化观察体(normalized view volume)中,其实也就是设备标准化坐标系(NDC)中的坐标。
转换过程遵循评语投影的规范化过程。从棱台观察体变换而来的矩形平行管道映射到对称左手参考系的规范化立方体中。完成规范化的缩放矩阵是:
将透视矩阵与缩放矩阵综合得到规范化矩阵:
将该规范化透视矩阵进行一般化,可以得到以下形式:
如果透视投影观察体一开始就指定为对称棱台,则可用裁剪窗口的视场角和尺寸来表达规范化透视变换的元素。将投影参考点位于原点且观察平面在近裁剪平面位置时,可以得到:
三、android OpenGLES 的三维观察函数
1、观察变换函数
在Android中,你可以使用Matrix.setLookAtM方法来设置观察变换矩阵,方法原型如下:
在OpenGL中的方法跟Android中的OpenGLES 不一样。OpenGL建模观察模式用下列语句来设定的:
观察参数用GLU函数指定,该函数如下:
其中(x0, y0, z0) 跟Android中的方法setLookAtM的(eyeX, eyeY, eyeZ) 点均表示观察参考点在世界坐标系的位置。而(xref, yref,zref) 和 (centerX, centerY, centerZ) 表示参考点的坐标,(Vx, Vy, Vz) 和 (upX, upY, upZ) 表示向上向量。默认情况下 gluLookAt的参数是 P0 = (0, 0, 0), Pref = (0,0, -1), V = (0, 1, 0);
2、对称透视投影棱台
OpenGL中对称透视投影棱台观察体用gluPerspective表示,原型如下:
在Android的OpenGLES 中也存在类似的方法:
其中 theta 和 fovy 表示视场角,0~180度可选。aspect表示长宽比。far 和near 表示观察参考点到远近裁剪平面的距离。
3、通用透视投影函数
在OpenGL中,通用棱台一般使用glFrustum函数来实现:
当选择 xwmin = - xwmax 且 ywmin = - ywmax 时,表示的是一个对称棱台。
在Android的OpenGLES中,可以使用类似的方法:
四、空间坐标投影到观察平面上的UV坐标计算。
好了,至此,我们讨论了透视投影矩阵变换的整个过程,以及OpenGL 和OpenGLES 设置投影矩阵的方法。那么,如何求得空间坐标经过透视投影矩阵的变换后,得到屏幕的UV坐标呢?
假设在棱台(frustum)中的一点的坐标为(x, y, z),经过投影变换后的坐标为(x', y', z')。则我们可以得到以下计算公式:
新得到的坐标(x', y', z', w) 是经过透视投影变换后的坐标,该坐标就是前面第7小节规范化透视投影变换坐标中讨论的透视投影齐次坐标。由于OpenGL的观察平面就是近裁剪平面,因此该坐标就是坐标(x,y,z)经过透视变换后在近裁剪平面上的三维坐标。其中w记录了深度信息。当设置为对称棱台投影后,矩阵的实际值就是前面计算得到的对称棱台规范化的透视投影矩阵:
那么换算得到的新坐标是投影齐次坐标,那么接下来如何转换成屏幕UV坐标呢?我们只需要将得到的新坐标进行归一化为NDC坐标系,然后将其转成UV坐标的表达形式即可。
1、转成NDC坐标系
这里将所有坐标均除以w值,即可得到NDC坐标,此时的w将被规整为-1 ~ 1 之间。
2、换算成屏幕UV坐标
根据前面计算得到的NDC坐标,可以换算成屏幕的UV坐标。由于NDC坐标的范围时-1 ~ 1 的立方体,而屏幕UV坐标则是0~1的平面。如何计算? 其实很简单,只需要从NDC坐标中缩小到原来的一般然后平移到UV屏幕中间(0.5, 0.5),即可求得UV坐标。
五、OpenGLES 中计算透视变换到裁剪平面上的uv坐标
看到前面的一大堆计算,估计很多人都会头晕眼花的。其实你完全可以不了解前面的推导过程,在OpenGL 和OpenGLES 中经过透视投影矩阵变换后的屏幕uv坐标甚至是一件非常简单的事情,变换过程如下:
通过将棱台(frustum)中的坐标,与总变换mvpMatrix的乘积,得到一个vec4的变量,该变量就是gl_Position的值。gl_Position代表着总变换后的空间位置。也就是我们所说的齐次坐标。然后将其转成NDC坐标,重新构建一个新的屏幕UV坐标即可。
六、最后说一下3D贴纸需求实现思路
实现3D贴纸并将贴纸与图像按照自定义的方式进行混合的整体过程实现如下:
1、根据人脸关键点坐标反推算出正脸的坐标(考虑歪头、转脸的情况,如果人脸关键点检测得到的是三维空间点另外计算)
2、根据正脸的坐标计算出贴纸处于屏幕空间四个顶点的UV坐标
3、根据计算出的贴纸处于屏幕空间的UV坐标,反推算出贴纸实际所处平面的空间坐标(假设未做歪头等动作时的坐标)
4、根据得到的贴纸实际空间的坐标,经过平移、旋转后,计算出投影变换后的综合矩阵。
5、在glsl中计算总最终的位置:
6、将得到的最终的空间坐标vPosition传递给Fagment Shader,将坐标转换成屏幕的uv坐标
7、获取图像与贴纸重叠部分的texture进行混合
∑GL-透视投影矩阵的推导
计算机显示器是二维表面。由OpenGL渲染的3D场景必须作为2D图像投影到计算机屏幕上。投影矩阵用于此投影变换。首先,它将所有顶点数据从眼睛坐标转换为剪裁坐标。然后,通过与剪裁坐标的w分量相除,这些剪裁坐标也被转换为归一化设备坐标(NDC)。
剪裁坐标:眼睛坐标现在与投影矩阵相乘,成为剪裁坐标。该投影矩阵定义了视锥体——顶点数据投影到屏幕上的方式(透视或正交)。之所以称为剪裁坐标,是因为变换后的顶点(x,y,z)是通过与±wclip进行比较来剪裁的。
视锥体剔除(裁剪)操作是在剪裁坐标中执行的,正好在除以wclip之前。通过与wclip的比较,测试了剪裁坐标xclip、yclip和zclip。因为除以wclip后成为归一化的NDC坐标,所以要满足-1<=xclip/wclip<=1,所以xclip∈[-wclip,wclip];同理yclip∈[-wclip,wclip],zclip∈[-wclip,wclip]。如果任何剪裁坐标小于-wclip或大于wclip,则顶点将被丢弃(剪裁掉)。
因此,我们必须记住,裁剪(视锥体剔除)和NDC变换都集成到投影矩阵中。以下介绍如何从6个参数构建投影矩阵:left、right、bottom、top、near和far边界值。
然后,OpenGL将重建发生剪裁的多边形的边(如下图中的两条红线)。
下图中的灰色区域即是保留而未被丢弃的点,满足:xc,yc,zc∈(-wc,wc)
下图展示了透视视锥体和归一化设备坐标(NDC):
在透视投影中,视锥体的锥台(眼睛坐标)中的3D点被映射到立方体(NDC);x坐标从[l,r]到[-1,1],y坐标从[b,t]到[-1,1],z坐标从[-n,-f]到[-1,1]。
void glFrustum
(
// 指定左右垂直剪裁平面的坐标。
GLdouble left, GLdouble right,
// 指定底部和顶部水平剪裁平面的坐标。
GLdouble bottom, GLdouble top,
// 指定到近深度剪裁平面和远深度剪裁平面的距离。两个距离都必须为正。
GLdouble nearVal, GLdouble farVal
);
请注意,眼睛坐标是在右手坐标系中定义的,但NDC使用左手坐标系。也就是说,原点处的相机在眼睛空间中沿-Z轴观看,但在NDC中沿+Z轴观看。由于glFrustum()只接受near和far的正值,因此我们需要在构造投影矩阵时对它们求反。
在OpenGL中,眼睛空间中的一个3D点被投影到近平面(投影平面)上。下图显示了眼睛空间中的一个点(xe,ye,ze)如何投影到近平面上的(xp,yp,zp)。
从视锥体的[俯视图],即眼睛空间的x坐标,xe被映射到xp,xp是通过使用相似三角形的比率来计算的:
从视锥体的侧面来看,yp也以类似的方式计算:
注意xp和yp都依赖于ze;它们与-ze成反比。换句话说,它们都被-ze除。这是构造投影矩阵的第一条线索。通过乘以投影矩阵变换眼睛坐标后,剪裁坐标仍然是齐次坐标。它最终成为标准化设备坐标(NDC),除以剪裁坐标的w分量。(更多细节,见OpenGL_Transformation)
因此,我们可以将剪裁坐标的w分量设置为-ze。投影矩阵的第4行变成(0,0,-1,0)。
接下来,我们用线性关系将xp和yp映射到NDC的xn和yn;[l,r]⇒ [-1,1]和[b,t]⇒ [-1,1]。
然后,我们将xp和yp代入上述方程。
注意,对于透视除法(xc/wc,yc/wc),我们使每个方程的两项都可以被-ze整除。我们在前面将wc设置为-ze,括号内的术语变成了剪裁坐标的xc和yc。
从这些方程中,我们可以找到投影矩阵的第1行和第2行。
现在,我们只需要解第3行投影矩阵。发现zn与其他的有点不同,因为眼睛空间中的ze总是投射到近平面上的-n。但是我们需要唯一的z值来进行裁剪和深度测试。此外,我们应该能够取消投影(逆变换)。因为我们知道z不依赖于x或y值,所以我们借用w分量来寻找zn和ze之间的关系。因此,我们可以这样指定投影矩阵的第3行。
在眼睛空间中,we等于1。因此,方程变为:
为了求系数A和B,我们使用(ze,zn)关系:(-n,-1)和(-f,1),并将它们放入上述方程中,解出A、B。
此时,ze与zn的关系变为:
最后,我们找到了投影矩阵的所有条目。完整的投影矩阵是:
该投影矩阵适用于一般视锥体。如果视体是对称的,即r=-l, t=-b,则可以简化为:
在我们继续之前,请再次看看ze和zn之间的关系,等式(3)。你注意到这是一个有理函数,ze和zn之间是非线性关系(如下图)。这意味着在近平面的精度非常高,但在远平面的精度非常低。如果射程[-n,-f]越来越大,则会导致深度精度问题(z-fighting);远平面附近ze的微小变化不影响zn值。n和f之间的距离应尽可能短,以最小化深度缓冲精度问题。
使用FOV指定的透视投影:
h = 2 × near × tan(θ/2)
w = h × aspect;
对应上述透视投影矩阵中:
r = w/2;t = h/2
所以:
n/r = (2×n)/w
= (2×n)/(h × aspect)
= (2×n)/(2 × n × tan(θ/2) × aspect)
= cot(θ/2)/aspect
同理求得,n/t = cot(θ/2)
则得到透视投影矩阵为:
以上是关于OpenGLES 透视变换与屏幕UV坐标的主要内容,如果未能解决你的问题,请参考以下文章