作者:位元組流動
來源:
https://blog.csdn.net/Kennethdroid/article/details/108425267 FFmpeg 開發系列連載:- FFmpeg 開發(01):FFmpeg 編譯和內建
- FFmpeg 開發(02):FFmpeg + ANativeWindow 實作視訊解碼播放
- FFmpeg 開發(03):FFmpeg + OpenSLES 實作音頻解碼播放
- FFmpeg 開發(04):FFmpeg + OpenGLES 實作音頻可視化播放
- FFmpeg 開發(05):FFmpeg + OpenGLES 實作視訊解碼播放和視訊濾鏡
- FFmpeg 開發(06):FFmpeg 播放器實作音視訊同步的三種方式
前文中,我們已經利用 FFmpeg + OpenGLES + OpenSLES 實作了一個多媒體播放器,本文将基于此播放器實作一個酷炫的 3D 全景播放器。
全景播放器原理
全景視訊是由多台錄影機在一個位置同時向四面八方拍攝,最後經過後期拼接處理生成的。

用普通的多媒體播放器播放全景視訊,畫面會呈現出嚴重的拉伸和扭曲變形。
全景播放器将視訊畫面渲染到球面上,相當于從球心去觀察内部球面,觀察到的畫面 360 度無死角,這也就是市面上大多數“ VR 盒子”的實作原理。
建構球面網格
全景播放器原理與普通播放器的本質差別在渲染圖像部分,普通播放器隻需将視訊畫面渲染到一個矩形平面上,而全景播放器需要将視訊畫面渲染到球面。
為實作全景播放器,我們隻需要利用 OpenGL 建構一個球體,然後将 FFmpeg 解碼的視訊畫面渲染到這個球體表面即可。
OpenGL ES 中所有 3D 物體均是由三角形構成的,建構一個球體隻需要利用球坐标系中的經度角、次元角以及半徑計算出球面點的三維坐标,最後這些坐标點構成一個個小矩形,每個矩形就可以分成 2 個三角形。
在球坐标系中,利用經度角、次元角和半徑計算出球面點坐标公式如下:
根據上述公式計算球面頂點坐标的代碼實作, 其中 ANGLE_SPAN 為步長,RADIUS 為半徑,RADIAN 用于弧度轉換 。
//建構頂點坐标
for (float vAngle = 90; vAngle > -90; vAngle = vAngle - ANGLE_SPAN) {//垂直方向每隔 ANGLE_SPAN 度計算一次
for (float hAngle = 360; hAngle > 0; hAngle = hAngle - ANGLE_SPAN) {//水準方向每隔 ANGLE_SPAN 度計算一次
double xozLength = RADIUS * cos(RADIAN(vAngle));
float x1 = (float) (xozLength * cos(RADIAN(hAngle)));
float z1 = (float) (xozLength * sin(RADIAN(hAngle)));
float y1 = (float) (RADIUS * sin(RADIAN(vAngle)));
xozLength = RADIUS * cos(RADIAN(vAngle - ANGLE_SPAN));
float x2 = (float) (xozLength * cos(RADIAN(hAngle)));
float z2 = (float) (xozLength * sin(RADIAN(hAngle)));
float y2 = (float) (RADIUS * sin(RADIAN(vAngle - ANGLE_SPAN)));
xozLength = RADIUS * cos(RADIAN(vAngle - ANGLE_SPAN));
float x3 = (float) (xozLength * cos(RADIAN(hAngle - ANGLE_SPAN)));
float z3 = (float) (xozLength * sin(RADIAN(hAngle - ANGLE_SPAN)));
float y3 = (float) (RADIUS * sin(RADIAN(vAngle - ANGLE_SPAN)));
xozLength = RADIUS * cos(RADIAN(vAngle));
float x4 = (float) (xozLength * cos(RADIAN(hAngle - ANGLE_SPAN)));
float z4 = (float) (xozLength * sin(RADIAN(hAngle - ANGLE_SPAN)));
float y4 = (float) (RADIUS * sin(RADIAN(vAngle)));
//球面小矩形的四個點
vec3 v1(x1, y1, z1);
vec3 v2(x2, y2, z2);
vec3 v3(x3, y3, z3);
vec3 v4(x4, y4, z4);
//建構第一個三角形
m_VertexCoords.push_back(v1);
m_VertexCoords.push_back(v2);
m_VertexCoords.push_back(v4);
//建構第二個三角形
m_VertexCoords.push_back(v4);
m_VertexCoords.push_back(v2);
m_VertexCoords.push_back(v3);
}
}
對應球面坐标的紋理坐标計算,實際上就是計算固定行和列的網格點。
//建構紋理坐标,球面展開後的矩形
int width = 360 / ANGLE_SPAN;//列數
int height = 180 / ANGLE_SPAN;//行數
float dw = 1.0f / width;
float dh = 1.0f / height;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
//每一個小矩形,由兩個三角形構成,共六個點
float s = j * dw;
float t = i * dh;
vec2 v1(s, t);
vec2 v2(s, t + dh);
vec2 v3(s + dw, t + dh);
vec2 v4(s + dw, t);
//建構第一個三角形
m_TextureCoords.push_back(v1);
m_TextureCoords.push_back(v2);
m_TextureCoords.push_back(v4);
//建構第二個三角形
m_TextureCoords.push_back(v4);
m_TextureCoords.push_back(v2);
m_TextureCoords.push_back(v3);
}
}
用 OpenGL 劃線渲染球狀網格,測試建構的球體是否準确。
渲染全景視訊
計算好頂點坐标和紋理坐标後,剩下的就是簡單的紋理映射(紋理貼圖),不了解紋理映射的同學可以檢視這篇文章
紋理映射,篇幅有限,這裡不展開講述。
頂點坐标和紋理坐标初始化 VAO 。
// Generate VBO Ids and load the VBOs with data
glGenBuffers(2, m_VboIds);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vec3) * m_VertexCoords.size(), &m_VertexCoords[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vec2) * m_TextureCoords.size(), &m_TextureCoords[0], GL_STATIC_DRAW);
// Generate VAO Id
glGenVertexArrays(1, &m_VaoId);
glBindVertexArray(m_VaoId);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vec3), (const void *)0);
glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vec2), (const void *)0);
glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
glBindVertexArray(GL_NONE);
繪制視訊畫面。
// Use the program object
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);
GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix);
//綁定紋理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
GLUtils::setFloat(m_ProgramObj, "s_TextureMap", 0);
glDrawArrays(GL_TRIANGLES, 0, m_VertexCoords.size());
先繪制普通視訊,看看是啥樣兒。
最後繪制全景視訊。
聯系與交流
技術交流/擷取源碼可以添加我的微信:Byte-Flow
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。