FFmpeg 开发(07):FFmpeg + OpenGLES 实现 3D 全景播放器

简介: 前文中,我们已经利用 FFmpeg + OpenGLES + OpenSLES 实现了一个多媒体播放器,本文将基于此播放器实现一个酷炫的 3D 全景播放器。

作者:字节流动

来源:https://blog.csdn.net/Kennethdroid/article/details/108425267


FFmpeg 开发系列连载:

前文中,我们已经利用 FFmpeg + OpenGLES + OpenSLES 实现了一个多媒体播放器,本文将基于此播放器实现一个酷炫的 3D 全景播放器。

全景播放器原理

全景视频是由多台摄像机在一个位置同时向四面八方拍摄,最后经过后期拼接处理生成的。

image.png

用普通的多媒体播放器播放全景视频,画面会呈现出严重的拉伸和扭曲变形。

全景播放器将视频画面渲染到球面上,相当于从球心去观察内部球面,观察到的画面 360 度无死角,这也就是市面上大多数“ VR 盒子”的实现原理。

跳舞.gif

构建球面网格

全景播放器原理与普通播放器的本质区别在渲染图像部分,普通播放器只需将视频画面渲染到一个矩形平面上,而全景播放器需要将视频画面渲染到球面。

为实现全景播放器,我们只需要利用 OpenGL 构建一个球体,然后将 FFmpeg 解码的视频画面渲染到这个球体表面即可。

image.png

OpenGL ES 中所有 3D 物体均是由三角形构成的,构建一个球体只需要利用球坐标系中的经度角、维度角以及半径计算出球面点的三维坐标,最后这些坐标点构成一个个小矩形,每个矩形就可以分成 2 个三角形。

image.png

image.png

在球坐标系中,利用经度角、维度角和半径计算出球面点坐标公式如下:

image.png

根据上述公式计算球面顶点坐标的代码实现, 其中 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 划线渲染球状网格,测试构建的球体是否准确。

image.png

渲染全景视频

计算好顶点坐标和纹理坐标后,剩下的就是简单的纹理映射(纹理贴图),不了解纹理映射的同学可以查看这篇文章纹理映射,篇幅有限,这里不展开讲述。

顶点坐标和纹理坐标初始化 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());

先绘制普通视频,看看是啥样儿。


最后绘制全景视频。

全景.gif

联系与交流

技术交流/获取源码可以添加我的微信:Byte-Flow


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云视频云@凡科快图.png

相关文章
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
1761 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
编解码 语音技术 内存技术
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
《FFmpeg开发实战:从零基础到短视频上线》一书中的“5.1.2 把音频流保存为PCM文件”章节介绍了将媒体文件中的音频流转换为原始PCM音频的方法。示例代码直接保存解码后的PCM数据,保留了原始音频的采样频率、声道数量和采样位数。但在实际应用中,有时需要特定规格的PCM音频。例如,某些语音识别引擎仅接受16位PCM数据,而标准MP3音频通常采用32位采样,因此需将32位MP3音频转换为16位PCM音频。
520 0
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
|
XML 开发工具 Android开发
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
ExoPlayer最初是为了解决Android早期MediaPlayer控件对网络视频兼容性差的问题而推出的。现在,Android官方已将其升级并纳入Jetpack的Media3库,使其成为音视频操作的统一引擎。新版ExoPlayer支持多种协议,解决了设备和系统碎片化问题,可在整个Android生态中一致运行。通过修改`build.gradle`文件、布局文件及Activity代码,并添加必要的权限,即可集成并使用ExoPlayer进行网络视频播放。具体步骤包括引入依赖库、配置播放界面、编写播放逻辑以及添加互联网访问权限。
1596 1
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
368 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
753 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
Android开发 开发者
FFmpeg开发笔记(五十七)使用Media3的Transformer加工视频文件
谷歌推出的Transformer,作为Jetpack Media3架构的一部分,助力开发者实现音视频格式转换与编辑。Media3简化了媒体处理流程,提升了定制性和可靠性。Transformer可用于剪辑、添加滤镜等操作,其示例代码可在指定GitHub仓库中找到。要使用Transformer,需在`build.gradle`中添加相关依赖,并按文档编写处理逻辑,最终完成音视频转换任务。具体步骤包括配置剪辑参数、设置空间效果以及监听转换事件等。
381 0
FFmpeg开发笔记(五十七)使用Media3的Transformer加工视频文件
|
XML Java Android开发
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
GSYVideoPlayer是一款国产移动端视频播放器,支持弹幕、滤镜、广告等功能,采用IJKPlayer、Media3(EXOPlayer)、MediaPlayer及AliPlayer多种内核。截至2024年8月,其GitHub星标数达2万。集成时需使用新版Android Studio,并按特定步骤配置依赖与权限。提供了NormalGSYVideoPlayer、GSYADVideoPlayer及ListGSYVideoPlayer三种控件,支持HLS、RTMP等多种直播链接。
646 18
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
|
Linux 开发工具 Android开发
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
EasyPusher是一款国产RTSP直播录制推流客户端工具,支持Windows、Linux、Android及iOS等系统。尽管其GitHub仓库(安卓版:https://github.com/EasyDarwin/EasyPusher-Android)已多年未更新,但通过一系列改造,如升级SDK版本、迁移到AndroidX、指定本地NDK版本及更新Gradle版本等,仍可在最新Android Studio上运行。以下是针对Android Studio Dolphin版本的具体改造步骤。
345 3
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
|
编解码 移动开发 安全
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生
自互联网普及以来,流媒体技术特别是视频直播技术不断进步,出现了多种传输协议。早期的MMS由微软主导,但随WMV格式衰落而减少使用。RTSP由网景和RealNetworks联合提出,支持多种格式,但在某些现代应用中不再受支持。RTMP由Adobe开发,曾广泛用于网络直播,但因HTML5不支持Flash而受影响。HLS由苹果开发,基于HTTP,适用于点播。SRT和RIST均为较新协议,强调安全与可靠性,尤其SRT在电视直播中应用增多。尽管RTMP仍占一定市场,但SRT等新协议正逐渐兴起。
624 8
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生

热门文章

最新文章