零. 前言
之前一段时间一直和团队的其他小伙伴研究很炫酷的特效,遇到了很多掉头发的问题,幸好大家都很给力,一一给解决了。今天抽空来复盘和总结一下当时遇到的一些难点吧,就是标题所说的,透视纠正和坐标系这两个大难题~
一. 透视纠正
我们的特效需求支持了不规则的图形遮罩,也就意味着我们不能直接简单粗暴地取一个图形的最大最小四个顶点坐标。
于是我们想到初步的方案是用OpenCV识别出四边形的四个顶点位置,直接传给业务端进行渲染,想法很不错,确实识别出来了,但是却发现渲染出来的图像完全扭曲了
有种百思不得其解的感觉,后面查到文章才知道是透视纠正的原因。。
在我们执行渲染操作的时候,顶点着色器会要求我们返回一个含(x, y, z, w)的四维坐标,w称为比例因子,当w不为0时(一般设1),表示一个坐标,一个三维坐标的三个分量x,y,z用齐次坐标表示为变为x,y,z,w的四维空间,变换成三维坐标是方式是(x/w, y/w, z/w)。
w的作用可不仅仅是使一个顶点等比例压缩,他还有透视纠正的功能,如下面公式所示,当渲染操作进行纹理渲染的时候,他会根据当前渲染点到两个顶点的距离、以及两个顶点的w值进行透视纠正,可以看到,某个点w值越大,就离a点越远。w的设置符合近大远小的透视变换。
如果我们直接传入顶点坐标,使w=1,则原透视纠正公式就会变为线性插值,最终导致了纹理变形:
因此,为了使得纹理不变形,我们需要获取两个参数,一个是图像的外接矩形坐标,一个是将外接矩形变换为真实顶点坐标的透视变换矩阵。当透视变换矩阵(4*4)乘以外接矩形坐标(4*1)时,即可得到真实的顶点坐标,纹理插值也不会变形了。
至于怎么得到透视变换矩阵嘛,那是工具的事儿啦,大概原理就是根据外接矩形坐标、真实顶点坐标,调用OpenCV的透视变换函数求出来的。
二. 坐标系
通过上一章,我们可以知道,需要用工具产生的顶点坐标、透视矩阵确定最终的顶点坐标坐标。
但由于这个特效是Web、Android、iOS三端的,而且iOS端渲染还包含Metal渲染和OpenGL渲染,各种渲染机制的坐标系不完全相同,可以简单地区分为左手坐标系和右手坐标系,我们需要根据这些坐标系来为工具定制出具体的透视变换矩阵求解方案。
什么是左手坐标系和右手坐标系呢?顾名思义,需要伸出你的左手和右手,并作出两两垂直的手势,如下图所示,可以发现,当x轴和y轴方向一致的时候,z轴会朝向两个相反的地方。
OpenGL和OpenCV同属右手坐标系,工具正常求透视矩阵即可,但对于Metal的透视矩阵,我们在计算的过程中需要将y坐标置反,这样就相当于z轴翻转了,才可以适配左手坐标系。
再来看看他们x、y轴的方向,可以发现OpenGL和Metal是以中间点为原点,往右、往上递增,而OpenCV是以左上角为原点,往右、往下递增。所以工具求出顶点位置后还需要一发x、y坐标转换操作。
这里的originX和originY就是将OpenCV的坐标系y坐标取反后,归一化得到的真实坐标。
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;
在解决完顶点坐标翻转后,我们还需要留意OpenGL和Metal之间的纹理坐标系的差异,OpenGL纹理坐标系以左下角为原点,而Metal以左上角为原点。
OpenGL渲染——GPUImage提供的默认纹理坐标如下:
static const GLfloat noRotationTextureCoordinates[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
};
由此可见,对于OpenGL渲染,顶点的构建四个点分别是左下、右下、左上、右上。
而自研Metal链式渲染也采用了相同的纹理坐标数值:
- (NSArray *)defaultTextureCoordinates {
return @[
@0.0f, @0.0f,
@1.0f, @0.0f,
@0.0f, @1.0f,
@1.0f, @1.0f,
];
}
但由于纹理坐标系与OpenGL不一致,因此对于Metal渲染,顶点的构建四个点分别是左上、右上、左下、右下。
因此,对于同一个点来说,OpenGL的顶点y坐标还需要再翻转一次。得到以下代码:
OpenGL的四个顶点:
float oriVertices[] = {
originX, -originY,
originX + width, -originY,
originX, height - originY,
originX + width, height - originY,
};
Metal的四个顶点:
NSArray *result = @[
@(originX), @(originY),
@(originX+width), @(originY),
@(originX), @(originY-height),
@(originX+width), @(originY-height),
];
三. 总结
w坐标是透视变换的重要因子,以近大远小的方式决定了渲染图形的坐标和纹理纠正。
OpenGL、Metal的顶点坐标系、纹理坐标系各不相同,需要根据具体坐标系去决定顶点坐标和纹理坐标。
网友评论