全景視頻在播放的時候,能夠自由地旋轉視角。若是結合手機的陀螺儀,全景視頻在移動端能夠具有更好的瀏覽體驗。本文主要介紹如何基於 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 實現全景播放器