高性能实现OpenGL中渲染结果RGBA转YUV420P
先来盘点盘点
之前移动端视频编码一直是IOS做的好,如果没记错的话IOS从7.0开始就支持硬编码(videotoolbox),在IOS上开发,几乎不用手动去处理字节数据,无论是数据从摄像头或解码器过来,还是导入/导出到OpenGL,还是最终渲染结束传入编码器都直接使用CVPixelBuffer即可。
而Android方面从MediaCodec和SurfaceTexture上线之后情况有所好转,也几乎可以实现不修改字节数据而实现视频图像的传递和处理。
但是Android最大的问题就是要处理各种非主流机器。比如曾遇到过的一些奇葩问题,MediaCodec无论是编码还是解码一次只能打开一个,编码分辨率只能是16的倍数等等。当然还有个最常见的问题,就是7.0以下系统不支持H264 High Profile,这严重导致影响最后生成视频的图像质量和尺寸比。这就必然导致一个问题,在Android上很可能会用软编码作为兜底方案来应付奇葩机型。
那么问题来了,使用软编码,比如用X264,你需要从OpenGL中读取到RGBA,然后转YUV,然后还要重新排列字节顺序称为YUV420P。最后你会发现这个过程的耗时远大于X264的编码耗时。读取RGBA在OpenGLES 3.0之后用 GL_PIXEL_PACK_BUFFER + glMapBufferRange 已经可以做到很快了,这里不详细说明。这里主要讨论怎么高性能转YUV420P。
最入门的方式,使用ffmpeg的libswscale。非常慢。
高级一点的方式,编写ASM处理。也很慢。
还有一个问题就是,只要你做一次memcpy,对于1080P这样的图像大小,单个函数调用就要5ms以上。
所以本人一直在找更快的办法处理这个转换,最后找到一个解决办法,性能测试结果平均1ms(使用双缓存切换绘制,代价就是延迟一帧,实际图像帧是F1,F2,F3,F4....编码器得到的帧是F1,F1,F2,F3...)。
左边是数据源,是OpenGL里面当前渲染的结果。
右边是编码器,需要的格式是三个内存指针,YUV420P。
中间的过程大概是这样的:
假设渲染图像是1080x1920,竖屏2K画面。
1、先创建一个FBO绑定到一个纹理,搜集当前所有渲染效果,将最终要编码的图像存入这个纹理中。纹理尺寸1080x1920.
2、再创建一个FBO,绑定到三个纹理(这里需要GLSL #version 300es)用来分别存储最终的且连续的Y,U,V数据。纹理尺寸270x1920。然后将步骤1中的纹理,拉伸铺满方式绘制到这个FBO中。
3、由此得知纹理坐标像素步长 tex_stride_Y.x = 1.0f/1080; tex_stride_Y.y = 1.0f/1920;是 在shader中做4次采样,第一次采样是原坐标,第二次纹理坐标 x+1.0f*tex_stride_Y.x,第三次纹理坐标x+2.0f*tex_stride_Y.x,同理第四次采样需要偏移3个步长。得到4个RGBA的数据,然后分别乘以RGB转Y公式,得到4个Y值,分别存入当前fragColor.rgba。最终这个纹理读回来的数据不就全是Y了吗?
4、类似步骤3,但是处理UV相对复杂一些,所以这里需要用张图来说明。
假设一个“”田“”字形就是当前这个瘦长的FBO的渲染画布。UV的数据长度是Y的1/4,实际上它的面积就只有一个彩色区域那么大。但是如果你最终绘制成左边那样,你最后读出来的数据,还需要手动去移动内存,但是如果你绘制成右边这样,你需要的1/4数据刚好就在纹理内存buffer的前面1/4,这不是省事得多吗?(图中的彩色部分,需要由整个原图的U或者V去拉伸填满)。但是U和V的区域变小了,所以采样步长增大2倍 tex_stride_UV.x = 2.0f/1080; tex_stride_UV.y = 2.0f/1920;
这里就需要在纹理坐标上做一些换算了。
首先当纹理坐标的Y>0.25f时,是无效区域,忽略即可。
第二判断纹理坐标的X<0.5f时,说明是图像画布的左边区域,那么将这个坐标的x乘以2,y乘以4,X范围就从(0,0.5)变成了(0,1), Y范围从(0,0.25)变成了(0, 1)。然后再去采样,然后同理加上步长再采样3次旁边的像素,得到四个像素的U或者V放到当前fragColor的rgba里。
第三判断纹理坐标的X>=0.5时,说明是图像画布的右边区域,这里我们反过来看最终我们需要的U或者V的数据的情况,在最终的连续的内存里面,U的行长度是Y的行长度的一半,也就是Y linesize=1080,而U linesize=540。但是这里FBO画布像素个数Y是270,而UV是135。所以右边1/2的数据实际上是左边数据的第二行。如图中红色小格格,它的数据U实际上需要去采样左边红色小格格对应的图像。纹理坐标换算:X = 2.0f * (x-0.5f), Y = 4.0f * (y + tex_stride_UV.y)。
通过以上的方式绘制,1080宽的图像被压缩绘制到270的图像里,刚好一个color放原图相邻4个像素的Y。
而U和V的纹理后3/4忽略,前面1/4存放了完整的数据。