Metal渲染:实现旋转/翻转功能

本文主要讲解如何在使用Metal渲染的时候,实现画面的旋转/翻转功能。

 

在讲旋转和翻转前先理解两个坐标系统,Metal的NDC (Normalized Device Coordinate) 坐标系统,和纹理坐标。

Metal的NDC是一个原点在中心,边长为2个单位长度的正方体:

1578743463_7_w1016_h760.png

对于2D视频渲染,NDC坐标系统就是原点在中心,边长为2个单位长度的正方形。

对于2D纹理来说,其坐标原点在左上角,是边长为1个单位长度的正方形:

1579159406_79_w748_h612.png

我们要实现旋转和翻转有两种方法:

  • 对纹理坐标进行变换
  • 也可以在顶点函数中对NDC顶点坐标进行变换

从我们的上层需求来看,旋转都是绕画面中心点进行旋转,翻转也是绕画面居中的水平线(X轴)和垂直线(Y轴)进行翻转,如果对纹理进行操作,原点不在中心,虽然绕任意点做旋转,任意轴做翻转都能实现,但是计算会复杂很多。简化一些的话,我们可以先将坐标原点平移到(0.5, 0.5)的中心再基于原点做转换,最后再把坐标原点平移回去,最开始的一个版本就是这样实现的。

但NDC坐标系统,其原点本身就在中心,旋转翻转操作会方便很多,因此现在的实现中我们都是对NDC坐标(顶点坐标)进行操作。

 

翻转的实现

水平翻转就是绕NDC坐标系Y轴进行翻转,点(x, y, 1)绕Y转翻转后为(-x, y, 1),变换矩阵为:

matrix_float3x3 const GHorizontalFlipMatrix = {
    (simd_float3){-1, 0, 0},
    (simd_float3){0, 1, 0},
    (simd_float3){0, 0, 1}
};

垂直翻转就是绕NDC坐标系X轴进行翻转,点(x, y, 1)绕X轴翻转后为(x, -y, 1),变换矩阵为:

matrix_float3x3 const GVerticalFlipMatrix = {
    (simd_float3){1, 0, 0},
    (simd_float3){0, -1, 0},
    (simd_float3){0, 0, 1}
};

 

平面中一点绕另一点旋转

1578744829_33_w536_h332.png

如图所示,a点绕o点旋转angle角度后(逆时针旋转)到b点的坐标?假设o点为原点,则有计算公式:

b.x = a.x*cos(angle)  - a.y*sin(angle)

b.y = a.x*sin(angle) + a.y*cos(angle)

其中顺时针为负,逆时针为正,角度angle为弧度值。

在我们实现中只两个旋转角度,顺时针90度和逆时针90度,所有的旋转都是这两个旋转通过不断的组合最后叠加的结果。

这里需要注意的是,我们现在旋转的是NDC中的顶点坐标,因此与用户视角来说的的旋转图形(纹理)方向是相反的。

用户期望的画面顺时针旋转90度,也就是顶点坐标逆时针旋转90度(PI/2),cos(PI/2) = 0, sin(PI/2) = 1, 代入公式b点坐标为:

b.x = -a.y

b.y = a.x

变换矩阵如下:

matrix_float3x3 const GClockwiseMatrix = {
    (simd_float3){0, 1, 0},
    (simd_float3){-1, 0, 0},
    (simd_float3){0, 0, 1}
};

同样用户期望的画面逆时针旋转90度,即顶点坐标顺时针旋转90度(-PI/2),cos(-PI/2) = 0, sin(-PI/2) = -1, 代入公式b点坐标为:

b.x = a.y

b.y = -a.x

变换矩阵如下:

matrix_float3x3 const GAntiClockwiseMatrix = {
    (simd_float3){0, -1, 0},
    (simd_float3){1, 0, 0},
    (simd_float3){0, 0, 1}
};
 
在用户没有对视频进行操作的时候其旋转,翻转矩阵就是一个标准矩阵:
matrix_float3x3 const GIdentityMatrix = {
    (simd_float3){1, 0, 0},
    (simd_float3){0, 1, 0},
    (simd_float3){0, 0, 1}
}

每进行旋转/翻转操作后,就是在当前的变换矩阵基础(最开始是标准矩阵)上再乘上相应的翻转/旋转变换矩阵。

 

旋转存储

 
如果一个播放器在第二次打开某一个视频时需要保留其上次打开视频所做的旋转/翻转,应该如何做呢?
我们最容易想到的办法是:
记录顺时针/逆时针旋转和水平翻转/垂直翻转这些用户所做的动作串进行存储,然后在下一次打开同一个视频的时候,在播放视频前先在重做这一串动作
此方法是最简单最容易想到的,但是此方法有以下缺点:
1. 如果这个存储的动作串足够长的话,会导致在正式播放前这个旋转/翻转的“恢复”时间比较长
2. 这些动作串有冗余,比如连续两次水平翻转,或者一次顺时针加一次逆时针旋转。但是更复杂更长的动作串什么模拟或以抵消很难计算
所以显示,存储与恢复旋转/翻转的动作串并不是一个好方法。
 
还有一种方法:要进行存储与恢复的上层其实并不需要关心存储的数据格式,考虑到我们的旋转都是90度的倍数,翻转也都是绕X轴或者Y轴,因此这个3 x 3的变换矩阵,无论怎么样组合多少次相乘,每个位置只能是0,1,-1三种值,不可能有其它的float数值,因此我们可以将变换矩阵转换为一个无符号32位int值进行存储。每2个bit表示矩阵的一个位置, 3x3的矩阵需要18个bit位。
2bit位与矩阵元素值对应关系:
1579157092_8_w608_h208.png
 
通过union和位域的方式使用QLVCompactTranslateMatrix来方便将矩阵和整形进行转换,前面18个bit位被用来表示3x3矩阵的各个元素,最后14个bit未使用:
typedef union {
    struct {
        uint32_t m11:2;
        uint32_t m12:2;
        uint32_t m13:2;
        
        uint32_t m21:2;
        uint32_t m22:2;
        uint32_t m23:2;
        
        uint32_t m31:2;
        uint32_t m32:2;
        uint32_t m33:2;
        
        uint32_t reserved:14;
    } matrix;
    uint32_t value;
} QLVCompactTranslateMatrix;

 一些视频本身会有旋转/翻转的属性,如果产品需要对此情况要求进行自动纠正显示则如何处理?

最简单的想法是当SDK解析到视频本身的旋转/翻转属性时将合适的旋转/翻转矩阵加于渲染模块即好。但此种做法有一个问题,比如以下的例子:
 
1. 视频VideoA本身旋转属性为顺时针旋转90度
2. 在第一次播放此视频时,解析到其旋转属性,则给渲染模拟施加一个顺时针90度的矩阵,视频播放正常
3. 关闭视频VideA,此时会将顺时针旋转90度的矩阵进行保存
4. 第二次再打开视频VideoA,此时同样解析到其旋转属性为顺时针90度,施加顺时针90度的矩阵
5. 播放器加载后,恢复之前存储的变换矩阵
 
4和5不管以什么顺序,最后都会导致给视频VideA施加两个顺时针90度的变换矩阵,从而导致视频播放不正确。
因此我们需要将根据视频属性本身施加的变换矩阵用户通过界面入口进行的旋转/翻转矩阵进行分离。
 
因此最终施加给Metal坐标的矩阵变换及关系如下:
 

 

 

参考:

 

posted on 2020-04-26 19:39  课蜜黄蜂  阅读(985)  评论(0编辑  收藏  举报