将OpenGL画面导出为视频文件

想着这个东西以后应该还会用到,所以就整理一下,供以后参考。

先说一下大致的方法:

先将帧缓冲区中RGB颜色数据读入到内存中,如果每个R、G、B都用GLbyte来存的话,一个点就是3byte,如果要取的区域比较大,FPS又较高,则数据量会很大,内存会很快耗费掉一大块,尽管如此,还是推荐在渲染过程中只把数据先读到内存中,等渲染结束后再全部输出到文件中,因为每一帧数据量过大时输出到文件的过程会较慢,可能会影响渲染过程,虽然理论上想应该不会影响,但是在我这确实出现了问题,视频处理后会出现丢帧的现象。输出的文件叫做raw video,体积大的吓人,所以我们需要对其编码压缩,需要的话还要加上声音,这就要用到神通广大的ffmpeg了。


在说导出视频之前还需提一点:平滑fps。如果每帧的渲染任务量较小(大),则每帧的渲染时间就会较少(多),fps就会较大(小),但是视频的fps一般都不超过60,所以为了ffmpeg编码方便,我们需要控制渲染的fps,也就是raw video的fps,一般可选60,25,24,15等值,24是大多数电影的fps,也是人眼识别连贯图像的最低fps。

1
2
3
4
5
6
double time_current = glfwGetTime();
double time_accurate = frame_count / 1.0 / fps;
double time_delta = time_accurate - time_current;
time_delta = time_delta > 0 ? time_delta : 0;
if(print) printf("frame_count:%d time_accurate:%lf time_current:%lf time_delta:%lf\n", frame_count, time_accurate, time_current, time_delta);
usleep(time_delta * 1000000);

只需将以上代码加到每帧最后即可,通过挂起强制平滑fps,与实际时间保持同步。


接下来说具体步骤:

  1. 在渲染前先建立一个存储帧的数组GLbyte *frame[MAX_FRAME]和帧计数器int frame_count = 0

    1
    2
    GLbyte *frame[MAX_FRAME];
    int frame_count = 0;
  2. 在渲染完每一帧后对frame中的指针new出一块固定大小的内存,该大小由要取的区域大小来定,然后用glReadPixes()将帧缓冲区中的RGB颜色数据读入到内存中。glReadPixel()用法见官方文档:glReadPixel()

    1
    2
    frame[frame_count] = new GLbyte[window_width * window_height * 3];
    glReadPixels(0, 0, window_width, window_height, GL_RGB, GL_UNSIGNED_BYTE, frame[frame_count++]);
    • 我这里是读取了整个窗口帧缓冲区。
    • 网上说glReadPixes()读取的是当前显示的帧的缓冲区,即前帧缓冲区,如果要读取刚刚填充好的但并未显示的帧缓冲区,需先交换帧缓冲区。不过我感觉无所谓,读哪个都一样,所以直接在交换之前读了。我也试了交换之后读,感觉并无异样。
  3. 渲染过程结束后,将帧数组中的数据直接写入文件中即可,数据量太大,写入过程会很久。比如我渲染的一个30s的过程,FPS为32,帧缓冲区大小为1920x1080,输出的文件竟有6个G!也可直接算出来:1920x1080x3x32x30/(2^30)=5.56。

    1
    2
    FILE *out = fopen("raw_video", "wb");
    for(int i = 0; i < frame_count; i++) fwrite(frame[i], window_width * window_height * 3, 1, out);
  4. ffmpeg编码压缩并加上声音

    1
    ffmpeg -f rawvideo -pixel_format rgb24 -video_size 1920x1080 -framerate 32 -i raw_video -i sound.wav -strict -2 -vf "vflip" result.mp4
    • -f rawvideo指定文件是raw video格式,即直接存储每一帧的全部点的数据;
    • -pixel_format rgb24指定点的格式是RGB格式,每个点24bit,也就是3byte;
    • -video_size 1920x1080指定视频小;
    • -framerate 32指定帧的速率FPS,即每秒的帧数;
    • -i raw_video指定输入文件为raw_video,也就是之前渲染的输出文件,文件名可任取;前面的四个选项都是针对此输入文件设定的;
    • -i sound.wav指定另一个输入文件sound.wav,也就是要加上的声音文件,如果想用其他格式,只需改变后缀名即可,只要ffmpeg支持;
    • -strict -2是为了开启处于试验阶段的AAC音频编码方式;(我的理解)
    • -vf "vflip"中的-vf表示视频过滤器,后面引号中的为过滤器内容,vflip表示将视频上下颠倒,因为用glReadPixels()读取的第一个点在区域中的左下角,而ffmpeg编码压缩的时候认为第一个点在左上角;
    • result.mp4为输出文件,如果想获得其他格式,只需改变后缀名即可,只要ffmpeg支持。

这样就可以得到一个经过编码压缩且有声音的视频文件了,如果不想加声音,去掉-i sound.wav即可。


如果想要给编码压缩后的视频文件加上声音,可用如下命令:

1
ffmpeg -i result.mp4 -i sound.wav -strict -2 result_with_sound.mp4


如果想要截取一段视频或音频,可用如下命令:

1
ffmpeg -i input.mp4 -ss 1.5 -to 14.275 output.mp4

  • -ss 1.5指定从1.5秒开始;
  • -to 14.275指定到14.275秒结束。

之前写的可视化插件都是用这种方法做的视频,效果还不错。

ffmpeg功能过于强大,剪辑、合成、编码、解码等功能都能实现,一些复杂的功能则可以通过一些基础功能的组合来实现,所以学一点ffmpeg的使用还是很有必要的。