FFmpeg 開發(07):FFmpeg + OpenGLES 實現 3D 全景播放器


FFmpeg + OpenGLES 實現 3D 全景播放器

該文章首發於微信公眾號:字節流動

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(03, 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(12, 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM