Metal與圖形渲染八:透視糾正與坐標(biāo)系

零. 前言

之前一段時(shí)間一直和團(tuán)隊(duì)的其他小伙伴研究很炫酷的特效,遇到了很多掉頭發(fā)的問題,幸好大家都很給力,一一給解決了。今天抽空來復(fù)盤和總結(jié)一下當(dāng)時(shí)遇到的一些難點(diǎn)吧,就是標(biāo)題所說的,透視糾正和坐標(biāo)系這兩個(gè)大難題~

一. 透視糾正

我們的特效需求支持了不規(guī)則的圖形遮罩,也就意味著我們不能直接簡(jiǎn)單粗暴地取一個(gè)圖形的最大最小四個(gè)頂點(diǎn)坐標(biāo)。

于是我們想到初步的方案是用OpenCV識(shí)別出四邊形的四個(gè)頂點(diǎn)位置,直接傳給業(yè)務(wù)端進(jìn)行渲染,想法很不錯(cuò),確實(shí)識(shí)別出來了,但是卻發(fā)現(xiàn)渲染出來的圖像完全扭曲了

有種百思不得其解的感覺,后面查到文章才知道是透視糾正的原因。。

在我們執(zhí)行渲染操作的時(shí)候,頂點(diǎn)著色器會(huì)要求我們返回一個(gè)含(x, y, z, w)的四維坐標(biāo),w稱為比例因子,當(dāng)w不為0時(shí)(一般設(shè)1),表示一個(gè)坐標(biāo),一個(gè)三維坐標(biāo)的三個(gè)分量x,y,z用齊次坐標(biāo)表示為變?yōu)閤,y,z,w的四維空間,變換成三維坐標(biāo)是方式是(x/w, y/w, z/w)。

w的作用可不僅僅是使一個(gè)頂點(diǎn)等比例壓縮,他還有透視糾正的功能,如下面公式所示,當(dāng)渲染操作進(jìn)行紋理渲染的時(shí)候,他會(huì)根據(jù)當(dāng)前渲染點(diǎn)到兩個(gè)頂點(diǎn)的距離、以及兩個(gè)頂點(diǎn)的w值進(jìn)行透視糾正,可以看到,某個(gè)點(diǎn)w值越大,就離a點(diǎn)越遠(yuǎn)。w的設(shè)置符合近大遠(yuǎn)小的透視變換。

如果我們直接傳入頂點(diǎn)坐標(biāo),使w=1,則原透視糾正公式就會(huì)變?yōu)榫€性插值,最終導(dǎo)致了紋理變形:

因此,為了使得紋理不變形,我們需要獲取兩個(gè)參數(shù),一個(gè)是圖像的外接矩形坐標(biāo),一個(gè)是將外接矩形變換為真實(shí)頂點(diǎn)坐標(biāo)的透視變換矩陣。當(dāng)透視變換矩陣(4*4)乘以外接矩形坐標(biāo)(4*1)時(shí),即可得到真實(shí)的頂點(diǎn)坐標(biāo),紋理插值也不會(huì)變形了。

至于怎么得到透視變換矩陣嘛,那是工具的事兒啦,大概原理就是根據(jù)外接矩形坐標(biāo)、真實(shí)頂點(diǎn)坐標(biāo),調(diào)用OpenCV的透視變換函數(shù)求出來的。

二. 坐標(biāo)系

通過上一章,我們可以知道,需要用工具產(chǎn)生的頂點(diǎn)坐標(biāo)、透視矩陣確定最終的頂點(diǎn)坐標(biāo)坐標(biāo)。

但由于這個(gè)特效是Web、Android、iOS三端的,而且iOS端渲染還包含Metal渲染和OpenGL渲染,各種渲染機(jī)制的坐標(biāo)系不完全相同,可以簡(jiǎn)單地區(qū)分為左手坐標(biāo)系和右手坐標(biāo)系,我們需要根據(jù)這些坐標(biāo)系來為工具定制出具體的透視變換矩陣求解方案。

什么是左手坐標(biāo)系和右手坐標(biāo)系呢?顧名思義,需要伸出你的左手和右手,并作出兩兩垂直的手勢(shì),如下圖所示,可以發(fā)現(xiàn),當(dāng)x軸和y軸方向一致的時(shí)候,z軸會(huì)朝向兩個(gè)相反的地方。

OpenGL和OpenCV同屬右手坐標(biāo)系,工具正常求透視矩陣即可,但對(duì)于Metal的透視矩陣,我們?cè)谟?jì)算的過程中需要將y坐標(biāo)置反,這樣就相當(dāng)于z軸翻轉(zhuǎn)了,才可以適配左手坐標(biāo)系。

再來看看他們x、y軸的方向,可以發(fā)現(xiàn)OpenGL和Metal是以中間點(diǎn)為原點(diǎn),往右、往上遞增,而OpenCV是以左上角為原點(diǎn),往右、往下遞增。所以工具求出頂點(diǎn)位置后還需要一發(fā)x、y坐標(biāo)轉(zhuǎn)換操作。

這里的originX和originY就是將OpenCV的坐標(biāo)系y坐標(biāo)取反后,歸一化得到的真實(shí)坐標(biāo)。

GLfloat originX, originY, width, height;
originX = -1 + 2 * rect.origin.x / videoSize.width;
originY = 1 - 2 * rect.origin.y / videoSize.height;
width = 2 * rect.size.width / videoSize.width;
height = 2 * rect.size.height / videoSize.height;

在解決完頂點(diǎn)坐標(biāo)翻轉(zhuǎn)后,我們還需要留意OpenGL和Metal之間的紋理坐標(biāo)系的差異,OpenGL紋理坐標(biāo)系以左下角為原點(diǎn),而Metal以左上角為原點(diǎn)。

OpenGL渲染——GPUImage提供的默認(rèn)紋理坐標(biāo)如下:

static const GLfloat noRotationTextureCoordinates[] = {
        0.0f, 0.0f,
        1.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 1.0f,
    };

由此可見,對(duì)于OpenGL渲染,頂點(diǎn)的構(gòu)建四個(gè)點(diǎn)分別是左下、右下、左上、右上。

而自研Metal鏈?zhǔn)戒秩疽膊捎昧讼嗤募y理坐標(biāo)數(shù)值:

- (NSArray *)defaultTextureCoordinates {
    return @[
        @0.0f, @0.0f,
        @1.0f, @0.0f,
        @0.0f, @1.0f,
        @1.0f, @1.0f,
    ];
}

但由于紋理坐標(biāo)系與OpenGL不一致,因此對(duì)于Metal渲染,頂點(diǎn)的構(gòu)建四個(gè)點(diǎn)分別是左上、右上、左下、右下。

因此,對(duì)于同一個(gè)點(diǎn)來說,OpenGL的頂點(diǎn)y坐標(biāo)還需要再翻轉(zhuǎn)一次。得到以下代碼:

OpenGL的四個(gè)頂點(diǎn):

float oriVertices[] = {
    originX,            -originY,
    originX + width,    -originY,
    originX,            height - originY,
    originX + width,    height - originY,
};

Metal的四個(gè)頂點(diǎn):

NSArray *result = @[
    @(originX), @(originY),
    @(originX+width), @(originY),
    @(originX), @(originY-height),
    @(originX+width), @(originY-height),
];

三. 總結(jié)

w坐標(biāo)是透視變換的重要因子,以近大遠(yuǎn)小的方式?jīng)Q定了渲染圖形的坐標(biāo)和紋理糾正。

OpenGL、Metal的頂點(diǎn)坐標(biāo)系、紋理坐標(biāo)系各不相同,需要根據(jù)具體坐標(biāo)系去決定頂點(diǎn)坐標(biāo)和紋理坐標(biāo)。

四. 參考文章

WebGL 紋理映射的透視糾正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容