内容简介:全景视频在播放的时候,可以自由地旋转视角。如果结合手机的陀螺仪,全景视频在移动端可以具备更好的浏览体验。本文主要介绍如何基于首先看一下最终的效果:
全景视频在播放的时候,可以自由地旋转视角。如果结合手机的陀螺仪,全景视频在移动端可以具备更好的浏览体验。本文主要介绍如何基于 AVPlayer
实现一个全景播放器。
首先看一下最终的效果:
在上一篇文章中,我们了解了如何对视频进行图形处理。(如果还不了解的话,建议先阅读一下。传送门)
一般全景视频的编码格式与普通视频并无区别,只不过它的每一帧都记录了 360 度的图像信息。全景播放器需要做的事情是,可以通过参数的设置,播放指定区域的图像。
所以,我们需要 实现一个滤镜,这个滤镜可以接收一些角度相关的参数,渲染指定区域的图像。然后我们再将这个滤镜,通过上一篇文章的方式,应用到视频上,就可以实现全景播放器的效果。
一、构造球面
全景视频的每一帧图像,其实是一个球面纹理。所以,我们第一步要做的是先构造球面,然后把纹理贴上去。
首先来看一段代码:
/// 生成球体数据 /// @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 这个库。它通过 分割数 和 球半径 ,生成了 顶点数组 和 索引数组 。
现在来逐行解释代码的含义:
(1)这部分代码是对原始图像进行分割。下面以 slices = 10
为例进行讲解:
如图, 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]; } }
以上所述就是小编给大家介绍的《使用 OpenGL ES 实现全景播放器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 实现一个简单的音乐播放器
- React实现H5在线音乐播放器
- vue实现自定义H5视频播放器
- JiaoZiVideoPlayer 6.2.1 发布,实现自定义播放器
- 借助 Turbolinks 实现不间断的网页音乐播放器
- 音视频学习 (十二) 基于 FFmpeg + OpenSLES 实现音频万能播放器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
AJAX HACKS中文版
帕里 / 2007-3 / 55.00元
《AJAX HACKS中文版:创建快速响应Web站点的工具和技巧》完全挖掘出了Ajax技术的优点,以手把手的方式教您如何揭开Ajax的神秘面纱。每个hack代表了完成某个特定任务的精巧方法,从而为您节省了大量的时间。 《AJAX HACKS中文版:创建快速响应Web站点的工具和技巧》搜集了80个有关Ajax技术的技巧,覆盖了该技术的所有亮点。你现在就想构建下一代Web应用吗?《AJAX HA......一起来看看 《AJAX HACKS中文版》 这本书的介绍吧!