OpenGLES 透視變換與屏幕UV坐標

一、從需求說起
本人在做3D貼紙的時候,遇到這樣的一個需求,在3D貼紙需要和圖像進行混合。做遠小近大的3D效果,需要將二維的貼紙經過透視變換繪制到屏幕上,如果要添加混合效果,則必須知道變換后的坐標,如果坐標對不上,則會導致混合后的貼紙繪制到屏幕上可能會出現錯亂、重疊的情況,因此如何計算重疊部分的準確坐標是實現混合的關鍵。為此,重新拾起已經被遺忘了的數學知識,將透視變換過程重新推導一遍,以便更好地理解透視變換以及如何轉換到屏幕的空間坐標的。

二、透視變換推導
1、透視投影公式
下圖給出了一個空間點(x, y, z)到一般的投影參考點(xv, yv, zv)的投影路徑。該投影線與觀察平面交于坐標位置(xp, yp, zvp), 其中zvp是觀察平面上選擇的位于z軸上的點。


投影點.png

由此我們可以計算得到坐標位置的參數方程如下:


參數方程.png

坐標(x', y', z') 代表沿投影線的任意一點。當 u = 0 時,位置為(x, y, z),當 u = 1 時位置為(xv, yv, zv)。
在觀察平面上,由 z' = zvp,可以求解z' 的方程得到沿投影線該位置的u參數如下:
u參數.png

將u代入方程,可得一般的投影變換公式:
投影變換公式.png

當投影參考點在Z軸上時,xv = yv = 0,此時有:


投影變換公式.png

2、透視投影的滅點
一組投影平行線匯聚到在一起的點稱為滅點(vanishing point)。通過投影平面的方向可以控制滅點的數量,透視投影可以分為一點、兩點或三點投影。
3、投影的觀察體
通過觀察平面上指定一個矩形裁剪窗口可以得到一個觀察體。但觀察體的邊界面不再平行。透視投影觀察體常稱為視錐體(pyramid of vision),與眼睛或相機的視覺圓錐體相近。添加垂直于z軸(且與觀察平面平行)的近、遠平面后,切掉了無限、透視投影觀察體的一部分后,得到一個棱臺(frustum)觀察體。觀察平面位于遠近平面與投影參考點之間,在OpenGLES中,近平面就是觀察平面,也就是裁剪窗口。
4、透視投影變換矩陣:
我們不能使用等式的x和y坐標系數來形成透視矩陣的元素,因為系數的分母是z的函數。但可以通過三維齊次坐標表示:
齊次坐標.png

其中,齊次參數h為:
齊次參數.png

因此可以求得x和y的值為:
齊次坐標值.png

因此,我們可以建立一個變換矩陣將一個空間位置轉為齊次坐標位置,使得矩陣僅包含透視參數而不包含坐標值。然后觀察坐標系的透視投影變換分兩步實現。先用透視變換矩陣計算齊次坐標:


透視矩陣.png

Ph 是齊次點(xh, yh, zh, h) 的列矩陣表示, 而P是坐標(x, y, z, 1)的列矩陣表示,在實際當中,透視矩陣需要跟觀察矩陣(ViewMatrix)合并,然后將組合矩陣應用于場景的世界坐標描述以生成齊次坐標。
為了防止z軸除以齊次參數h后出現扭曲,我們需要通過為z變換設定矩陣元素從而對透視投影zp的坐標進行規范化。有多種方法選擇矩陣元素。下面是一種可能形成透視投影矩陣的方法:


投影矩陣.png

參數sz 和tz 是在對z坐標投影值規范化中的比例和平移因子。sz 和tz的特定值依賴于選擇的規范化范圍

5、對稱的視錐體
從投影參考點到裁剪窗口中心平穿過觀察體的線就會說透視投影棱臺的中心線。棱臺中心線與觀察平面相交于坐標(xv, yv, zvp)位置,用窗口尺寸表示裁剪窗口的對角位置,可得:


對角位置.png

整體如下圖所示:


對稱視錐體.png

或者,可以通過使用逼近相機鏡頭特性的參數來指定透視投影。視錐可以看做是一個視場角(field-of-view angle),他是照相機鏡頭的尺寸度量。如下圖所示:
視錐體.png

由上圖可以得到:
角度與裁剪窗口高度的關系.png

可以求得裁剪窗口高度:


窗口高度.png

上面的投影矩陣:
投影矩陣.png

中zv - zvp的對角線元素可以用下面表達式來表示:
矩陣對角線元素.png

其中aspect 是長寬比。

6、斜透視投影棱臺
如果透視投影觀察體中心線并不垂直于觀察平面,則得到一個斜棱臺(oblique frustum)。
為了方便計算,將投影參考點(xv, yv, zv) = (0, 0, 0),可得到錯切變換矩陣的元素:


錯切變換矩陣.png

如果將觀察平面放在近裁剪平面處,則透視投影矩陣可以進一步簡化。將裁剪窗口中心移到觀察平面坐標位置(0, 0)處,需要選擇的錯切參數值滿足:


錯切參數條件.png

當投影參考點位于觀察坐標原點切近裁剪平面與觀察平面重合時,透視投影矩陣可以簡化為:


透視投影矩陣簡化.png

將透視矩陣和錯切矩陣綜合,就可以得到下面的將場景坐標位置轉換成齊次正交坐標的斜透視投影矩陣。該變換的投影參考點是觀察坐標原點,而近裁剪平面是觀察平面。


斜透視投影矩陣.png

7、規范化透視投影變換坐標
矩陣將觀察坐標系中的對角位置變換到透視投影齊次坐標。使用齊次參數h 除齊次坐標,可得實際的正交投影坐標。該透視投影將棱臺觀察體中所有點變換成矩形平行管道觀察體中的位置,變換過程的最后一步是將該平行管道映射到規范化觀察體(normalized view volume)中,其實也就是設備標準化坐標系(NDC)中的坐標。
轉換過程遵循評語投影的規范化過程。從棱臺觀察體變換而來的矩形平行管道映射到對稱左手參考系的規范化立方體中。完成規范化的縮放矩陣是:


縮放矩陣.png

將透視矩陣與縮放矩陣綜合得到規范化矩陣:

透視投影規范化矩陣.png

將該規范化透視矩陣進行一般化,可以得到以下形式:


透視投影規范化變換.png

如果透視投影觀察體一開始就指定為對稱棱臺,則可用裁剪窗口的視場角和尺寸來表達規范化透視變換的元素。將投影參考點位于原點且觀察平面在近裁剪平面位置時,可以得到:

對稱棱臺規范化的透視投影.png

三、Android OpenGLES 的三維觀察函數
1、觀察變換函數
在Android中,你可以使用Matrix.setLookAtM方法來設置觀察變換矩陣,方法原型如下:

public static void setLookAtM(float[] rm, int rmOffset,
            float eyeX, float eyeY, float eyeZ,
            float centerX, float centerY, float centerZ, float upX, float upY,
            float upZ);

在OpenGL中的方法跟Android中的OpenGLES 不一樣。OpenGL建模觀察模式用下列語句來設定的:

glMatrixMode(GL_MODELVIEW);

觀察參數用GLU函數指定,該函數如下:

gluLookAt(x0, y0, z0, xref, yref, zref, Vx, Vy, Vz);

其中(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表示,原型如下:

gluPerspective(theta, aspect, dnear, dfar);

在Android的OpenGLES 中也存在類似的方法:

public static void perspectiveM(float[] m, int offset,
          float fovy, float aspect, float zNear, float zFar);

其中 theta 和 fovy 表示視場角,0~180度可選。aspect表示長寬比。far 和near 表示觀察參考點到遠近裁剪平面的距離。

3、通用透視投影函數
在OpenGL中,通用棱臺一般使用glFrustum函數來實現:

glFrustum(xwmin, xwmax, ywmin, ywmax, dnear, dfar);

當選擇 xwmin = - xwmax 且 ywmin = - ywmax 時,表示的是一個對稱棱臺。
在Android的OpenGLES中,可以使用類似的方法:

public static void frustumM(float[] m, int offset,
            float left, float right, float bottom, float top,
            float near, float far);

四、空間坐標投影到觀察平面上的UV坐標計算。
好了,至此,我們討論了透視投影矩陣變換的整個過程,以及OpenGL 和OpenGLES 設置投影矩陣的方法。那么,如何求得空間坐標經過透視投影矩陣的變換后,得到屏幕的UV坐標呢?
假設在棱臺(frustum)中的一點的坐標為(x, y, z),經過投影變換后的坐標為(x', y', z')。則我們可以得到以下計算公式:


透視投影換算.png

新得到的坐標(x', y', z', w) 是經過透視投影變換后的坐標,該坐標就是前面第7小節規范化透視投影變換坐標中討論的透視投影齊次坐標。由于OpenGL的觀察平面就是近裁剪平面,因此該坐標就是坐標(x,y,z)經過透視變換后在近裁剪平面上的三維坐標。其中w記錄了深度信息。當設置為對稱棱臺投影后,矩陣的實際值就是前面計算得到的對稱棱臺規范化的透視投影矩陣:


對稱棱臺規范化的透視投影矩陣.png

那么換算得到的新坐標是投影齊次坐標,那么接下來如何轉換成屏幕UV坐標呢?我們只需要將得到的新坐標進行歸一化為NDC坐標系,然后將其轉成UV坐標的表達形式即可。
1、轉成NDC坐標系
這里將所有坐標均除以w值,即可得到NDC坐標,此時的w將被規整為-1 ~ 1 之間。


NDC坐標.png

2、換算成屏幕UV坐標
根據前面計算得到的NDC坐標,可以換算成屏幕的UV坐標。由于NDC坐標的范圍時-1 ~ 1 的立方體,而屏幕UV坐標則是0~1的平面。如何計算? 其實很簡單,只需要從NDC坐標中縮小到原來的一般然后平移到UV屏幕中間(0.5, 0.5),即可求得UV坐標。


UV坐標.png

五、OpenGLES 中計算透視變換到裁剪平面上的uv坐標
看到前面的一大堆計算,估計很多人都會頭暈眼花的。其實你完全可以不了解前面的推導過程,在OpenGL 和OpenGLES 中經過透視投影矩陣變換后的屏幕uv坐標甚至是一件非常簡單的事情,變換過程如下:

vec2 calculateUVPosition(vec3 modelPosition, mat4 mvpMatrix) {
    vec4 tmp = vec4(modelPosition, 1.0);
    tmp = mvpMatrix * tmp; // gl_Position
    tmp /= tmp.w; // 經過這個步驟,tmp就是歸一化標準坐標了.
    tmp = tmp * 0.5 + vec4(0.5f, 0.5f, 0.5f, 0.5f); // NDC坐標
    return vec2(tmp.x, tmp.y); // 屏幕的UV坐標
}

通過將棱臺(frustum)中的坐標,與總變換mvpMatrix的乘積,得到一個vec4的變量,該變量就是gl_Position的值。gl_Position代表著總變換后的空間位置。也就是我們所說的齊次坐標。然后將其轉成NDC坐標,重新構建一個新的屏幕UV坐標即可。

六、最后說一下3D貼紙需求實現思路
實現3D貼紙并將貼紙與圖像按照自定義的方式進行混合的整體過程實現如下:
1、根據人臉關鍵點坐標反推算出正臉的坐標(考慮歪頭、轉臉的情況,如果人臉關鍵點檢測得到的是三維空間點另外計算)
2、根據正臉的坐標計算出貼紙處于屏幕空間四個頂點的UV坐標
3、根據計算出的貼紙處于屏幕空間的UV坐標,反推算出貼紙實際所處平面的空間坐標(假設未做歪頭等動作時的坐標)
4、根據得到的貼紙實際空間的坐標,經過平移、旋轉后,計算出投影變換后的綜合矩陣。
5、在glsl中計算總最終的位置:

vertex shader中:
varying vec4 vPosition;
...
vPosition = mvpMatrix * position;
 gl_Position = vPosition;

6、將得到的最終的空間坐標vPosition傳遞給Fagment Shader,將坐標轉換成屏幕的uv坐標

// 歸一化處理,轉成齊次坐標
vec4 position = vPosition / vPosition.w;
// 轉換成屏幕uv坐標
position = position * 0.5 + vec4(0.5, 0.5, 0.5, 0.5);

7、獲取圖像與貼紙重疊部分的texture進行混合

// 重疊部分的texture
vec4 source = texture2D(inputTexture,  position.xy);
// 貼紙的texture
vec4 mipmap = texture2D(sTexture, texCoord);
// 混合
gl_FragColor = blendColor(mipmap, source);
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容