全景视频在播放的时候,能够自由地旋转视角。若是结合手机的陀螺仪,全景视频在移动端能够具有更好的浏览体验。本文主要介绍如何基于 AVPlayer
实现一个全景播放器。ios
首先看一下最终的效果:git
在上一篇文章中,咱们了解了如何对视频进行图形处理。(若是还不了解的话,建议先阅读一下。传送门)github
通常全景视频的编码格式与普通视频并没有区别,只不过它的每一帧都记录了 360 度的图像信息。全景播放器须要作的事情是,能够经过参数的设置,播放指定区域的图像。swift
因此,咱们须要实现一个滤镜,这个滤镜能够接收一些角度相关的参数,渲染指定区域的图像。而后咱们再将这个滤镜,经过上一篇文章的方式,应用到视频上,就能够实现全景播放器的效果。数组
全景视频的每一帧图像,实际上是一个球面纹理。因此,咱们第一步要作的是先构造球面,而后把纹理贴上去。ide
首先来看一段代码:学习
/// 生成球体数据 /// @param slices 分割数,越多越平滑 /// @param radius 球半径 /// @param vertices 顶点数组 /// @param indices 索引数组 /// @param verticesCount 顶点数组长度 /// @param indicesCount 索引数组长度 - (void)genSphereWithSlices:(int)slices radius:(float)radius vertices:(float **)vertices indices:(uint16_t **)indices verticesCount:(int *)verticesCount indicesCount:(int *)indicesCount { // (1) int numParallels = slices / 2; int numVertices = (numParallels + 1) * (slices + 1); int numIndices = numParallels * slices * 6; float angleStep = (2.0f * M_PI) / ((float) slices); // (2) if (vertices != NULL) { *vertices = malloc(sizeof(float) * 5 * numVertices); } if (indices != NULL) { *indices = malloc(sizeof(uint16_t) * numIndices); } // (3) for (int i = 0; i < numParallels + 1; i++) { for (int j = 0; j < slices + 1; j++) { int vertex = (i * (slices + 1) + j) * 5; if (vertices) { (*vertices)[vertex + 0] = radius * sinf(angleStep * (float)i) * sinf(angleStep * (float)j); (*vertices)[vertex + 1] = radius * cosf(angleStep * (float)i); (*vertices)[vertex + 2] = radius * sinf(angleStep * (float)i) * cosf(angleStep * (float)j); (*vertices)[vertex + 3] = (float)j / (float)slices; (*vertices)[vertex + 4] = 1.0f - ((float)i / (float)numParallels); } } } // (4) if (indices != NULL) { uint16_t *indexBuf = (*indices); for (int i = 0; i < numParallels ; i++) { for (int j = 0; j < slices; j++) { *indexBuf++ = i * (slices + 1) + j; *indexBuf++ = (i + 1) * (slices + 1) + j; *indexBuf++ = (i + 1) * (slices + 1) + (j + 1); *indexBuf++ = i * (slices + 1) + j; *indexBuf++ = (i + 1) * (slices + 1) + (j + 1); *indexBuf++ = i * (slices + 1) + (j + 1); } } } // (5) if (verticesCount) { *verticesCount = numVertices * 5; } if (indicesCount) { *indicesCount = numIndices; } }
这段代码参考自 bestswifter/BSPanoramaView 这个库。它经过分割数和球半径,生成了顶点数组和索引数组。ui
如今来逐行解释代码的含义:编码
(1) 这部分代码是对原始图像进行分割。下面以 slices = 10
为例进行讲解:spa
如图,slices
表示分割的份数,横向被分割成了 10 份。numParallels
表示层数,纵向分割成 5 份。由于纹理贴到球面时,横向须要覆盖 360 度,纵向只须要覆盖 180 度,因此纵向分割数是横向分割数的一半。能够把它们想象成经纬度来帮助理解。
numVertices
表示顶点数,如图中蓝色点的个数。numIndices
表示索引数,当使用 EBO
绘制矩形的时候,一个矩形须要 6 个索引值,因此这里须要用矩形的个数乘以 6 。
angleStep
表示纹理贴到球面后,每一份分割对应的角度增量。
(2) 根据顶点数和索引数申请顶点数组和索引数组的内存空间。
(3) 开始建立顶点数据。这里遍历每个顶点,计算每个顶点的顶点坐标和对应的纹理坐标。
为了方便表示,将 角 AOB 记为 α ,将 角 COD 记为 β ,半径记为 r 。
当 i
和 j
都为 0
的时候,表示的是图中的 G 点。实际上,第一行的 11 个点都会和 G 点重合。
对于图中的 A 点,它的坐标为:
x = r * sin α * sin β y = r * cos α z = r * sin α * cos β
由此易得出顶点坐标的计算公式。
而纹理坐标只须要根据分割数等比增加。值得注意的是,因为纹理坐标的原点在左下角,因此纹理坐标的 y 值要取反,即 G 点对应的纹理坐标是 (0, 1)
。
(4) 计算每一个索引的值。其实很好理解,好比第一个矩形,它须要用到第一行的前两个顶点和第二行的前两个顶点,而后将这四个顶点拆成两个三角形来组合。
(5) 返回生成的顶点数组和索引数组的长度,在实际渲染的时候须要用到。由于每个顶点有 5 个变量,因此须要乘上 5 。
将上面生成的数据进行绘制,能够看到球面已经生成:
OpenGL ES 默认使用的是正射投影,正射投影的特色是远近图像的大小是同样的。
在这个例子中,咱们须要使用透视投影。透视投影定义了可视空间的平截头体,处于平截头体内的物体才会被以近大远小的方式渲染。
如图,咱们须要使用 GLKMatrix4MakePerspective(float fovyRadians, float aspect, float nearZ, float farZ)
来构造透视投影的变换矩阵。
fovyRadians
表示视野,fovyRadians
越大,视野越大。aspect
表示视窗的比例,nearZ
表示近平面,farZ
表示远平面。
在实际使用中,nearZ
通常设置为 0.1
,farZ
通常设置为 100
。
具体代码以下:
GLfloat aspect = [self outputSize].width / [self outputSize].height; CGFloat perspective = MIN(MAX(self.perspective, kMinPerspective), kMaxPerspective); GLKMatrix4 matrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(perspective), aspect, 0.1, 100.f);
由于摄像机的默认坐标是 (0, 0, 0)
,而球面的半径是 1
,处于 0.1 ~ 100
这个范围内。因此经过透视投影的矩阵变换后,看到的是从球面的内部,由平截头体截出来的图像。
由于是球面内部的图像,因此是镜像的(这个问题后面一块儿解决)。
手机设备内置有陀螺仪,能够实时获取到设备的 roll
、pitch
、yaw
信息,它们被称为欧拉角。
但凡使用过欧拉角,都会遇到一个万向节死锁问题,它能够用四元数来解决。因此咱们这里不直接读取设备的欧拉角,而是使用四元数,再把四元数转成旋转矩阵。
幸运的是,系统也提供四元数的直接访问接口:
CMQuaternion quaternion = self.motionManager.deviceMotion.attitude.quaternion;
可是获得的四元数还不能直接使用,须要作三步变换:
第一步: Y 轴取反
matrix = GLKMatrix4Scale(matrix, 1.0f, -1.0f, 1.0f);
考虑到前面 X 轴镜像的问题,因此这一步其实是:
matrix = GLKMatrix4Scale(matrix, -1.0f, -1.0f, 1.0f);
第二步: 顶点着色器 y 份量取反
// Panorama.vsh gl_Position = matrix * vec4(position.x, -position.y, position.z, 1.0);
第三步: 四元数 x 份量取反
CMQuaternion quaternion = self.motionManager.deviceMotion.attitude.quaternion; double w = quaternion.w; double wx = quaternion.x; double wy = quaternion.y; double wz = quaternion.z; self.desQuaternion = GLKQuaternionMake(-wx, wy, wz, w);
而后经过 self.desQuaternion
才能计算出正确的旋转矩阵。
GLKMatrix4 rotation = GLKMatrix4MakeWithQuaternion(self.desQuaternion); matrix = GLKMatrix4Multiply(matrix, rotation);
咱们在不断地移动手机时,self.desQuaternion
会不断地变化。因为移动手机的速度是变化的,因此 self.desQuaternion
的增量是不固定的。这样致使的结果是画面卡顿。
因此须要作平滑处理,在当前四元数和目标四元数之间,根据必定的增量进行线性插值。这样能保证镜头的移动不会发生突变。
float distance = 0.35; // 数字越小越平滑,同时移动也更慢 self.srcQuaternion = GLKQuaternionNormalize(GLKQuaternionSlerp(self.srcQuaternion, self.desQuaternion, distance));
在实际的渲染过程当中,外部能够进行渲染参数的调整,来修改渲染的结果。
好比以 perspective
为例,看一下在修改视野大小的时候,具体的参数是怎么传递的。
// MFPanoramaPlayerItem.m - (void)setPerspective:(CGFloat)perspective { _perspective = perspective; NSArray *instructions = self.videoComposition.instructions; for (MFPanoramaVideoCompositionInstruction *instruction in instructions) { instruction.perspective = perspective; } }
在 MFPanoramaPlayerItem
中,当 perspective
修改时,会从当前的 videoComposition
中获取到 MFPanoramaVideoCompositionInstruction
数组,再遍历赋值。
// MFPanoramaVideoCompositionInstruction.m - (void)setPerspective:(CGFloat)perspective { _perspective = perspective; self.panoramaFilter.perspective = perspective; }
在 MFPanoramaVideoCompositionInstruction
中,修改 perspective
会给 panoramaFilter
赋值。而后 MFPanoramaFilter
开始渲染的时候,在 startRendering
方法中,会根据 perspective
属性,生成新的变换矩阵。
因为 OpenGL ES 不支持后台渲染,因此要注意,在 APP 切换到后台前,应该暂停播放。
NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(willResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
- (void)willResignActive:(NSNotification *)notification { if (self.state == MFPanoramaPlayerStatePlaying) { [self pause]; } }
请到 GitHub 上查看完整代码。
获取更佳的阅读体验,请访问原文地址【Lyman's Blog】使用 OpenGL ES 实现全景播放器