手机淘宝(搜索框->摄像头->试妆魔镜):
最初的设计原型及性能问题:
- 单线程模型,优先级过低:从Camera获取到CMSampleBufferRef YUV图像帧,拷贝像素数据到内存(多了一次拷贝内存的开销)进行美妆渲染以及一些其他的检测计算,导致的render线程性能消耗过多,CPU负载过重,使用率在78%左右,直接导致了FPS过低,在5S机型上实测在 20 FPS 以下,非常的卡顿。
- OpenGL渲染通过UI主线程RunLoop回调:OpenGL 渲染在DrawRect中实现,通过setNeedsDisplay回调刷新,会有主线程被占用导致的屏幕卡顿的问题。
- 渲染全链路设计为单层FrameBuffer结构,所有渲染过程及OpenGL函数调用都在一个VC函数中,导致过多的渲染参数控制(每增加一个功能,就需要增加一个参数控制),并且与底层渲染模块存在较高的耦合度,导致此函数实现越来越臃肿复杂。
- FrameBuffer等一些资源的重复创建所带来的性能开销,以及多实例下的美妆模块重复的初始化导致出现多份拷贝,内存使用率不高,开销过大。
OpenGL ES 2.0:
- 更好的支持RGB32和YUV420像素格式转换以及Camera 分辨率切换。
- 更加方便灵活地控制整个渲染全链路以及每个可插拔式渲染节点,满足角点闪星或者美妆渲染层的即时生效和帧同步。
- 更好的性能及更高的FPS,一些角点检测,光线检测可异步计算减少更新同步频率, 保证Camera FPS的流畅性,一些像素格式转换,算法可移植到GPU(shader program),角点闪星效果从Core Animation移植OpenGL glDrawArrays,动效改为shader实现, 都极大地减少了CPU负载,合理的平衡CPU和GPU负载均衡,提升了屏幕刷新性能和FPS。
- 支持更加灵活的Capture取图方式,OpenGL支持获取每层渲染纹理的截图,支持更多功能需求,例如对比美妆前后效果,去除角点闪星渲染等功能。
OpenGL 可插拔式渲染全链路设计:
- 整个过程中的所有链路节点都继承至GLNode基类,通过Shader,提供对每帧纹理缓存(OpenGLFrameBuffer)的渲染能力,通过addTarget添加子节点集合,最终形成单链表渲染全链路。
- 渲染全链路以Input节点类为输入源起点,可以是一张图片,一段视频,或者是持续性的Camera视频输入,过程中可选择性加入角点检测,人脸侦测,美妆渲染,坐标变换滤镜等渲染节点,最后将最终渲染的一帧或多帧图像纹理缓存输出到Output节点(GlView或CAEAGLLayer)。
OpenGL 多线程优化:
- Input 线程主要提供每帧图像源数据,它的每帧图像处理性能直接决定屏幕显示的FPS,因此它设置为高优先级,一些比较耗时的CPU操作都采用异步线程去计算完成,如角点计算,光线强度计算,截屏等功能。
- Render 线程主要负责OpenGL ES 2.0 的全链路渲染,主线程中一些设置更改节点参数,添加移除部分链路节点,更改渲染全路径等UI操作都需要异步切换到此Render线程执行,为了确保内部数据线程安全。
- Detector 线程主要负责异步计算图像的角点信息和光线强度,通过边缘弱化(中心阈值越低,边缘阈值越高),压缩像素点采样(减少计算的像素点)从而提升性能,减少此线程对整体性能的影响,计算得到的结果数据通知Render线程同步更新OpenGL数据缓存,在接下来的渲染中,读取最新的结果数据进行显示。
更多优化细节:
纹理缓存:
- 通过对纹理的像素格式,大小,属性作为Hash Key标识,创建全局共享纹理(FrameBuffer)缓存队列,同时使用内存引用计数机制管理每一个纹理缓存,监听内存警告,当内存紧张时,释放所有缓存纹理,同时每个渲染节点都读取和使用缓存纹理进行每层纹理的更新与向下传递,减少了创建和销毁纹理的性能消耗。
Shader GPU优化:
- 耗时的像素转换(YUV420转换RGB32)计算通过调用GPU Shader执行计算:
mediump vec3 yuv;
lowp vec3 rgb;
yuv.x = texture2D(luminanceTexture, textureCoordinate).r;
yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);
rgb = YUV420ConverRGB32Matrix * yuv;
gl_FragColor = vec4(rgb, 1);
- 角点闪星动效通过调用GPU Shader执行渲染:
gl_Position = u_ProjectionMatrix * u_ModelViewMatrix * vec4(position, 1.0);
gl_PointSize = max(0.0, (u_eSize + a_pSizeOffset));
v_pColorOffset = a_pColorOffset;
highp vec4 texture = texture2D(u_Texture, gl_PointCoord);
highp vec4 color = vec4(0.0, 0.0, 0.0, 0.7);
color.rgb = u_eColor;
color.rgb += v_pColorOffset;
gl_FragColor = texture * color;
- 用GPU Shader 替换执行CPU计算代码,大大减少了CPU的运算负载,提升了实时渲染性能和FPS。
试妆魔镜节点共享初始化实例:
- 试妆魔镜模块算法模型执行异步初始化,成功后修改节点链路,添加试妆魔镜节点至OpenGL渲染全链路,当重复进入存在多个渲染全链路应用场景时,后续不需要再次初始化算法模型,可以直接使用已初始化的共享试妆魔镜实例执行渲染操作。
Detector图像计算性能优化:
- Camera输出的图像像素分辨率过高,直接进行计算消耗太多的性能,实际屏幕显示的尺寸也小于原有图像的比例,因此需要对原始图像进行压缩采样,减少像素计算的时间复杂度,同时为了更好的调控性能,争对不同的机器硬件设置不同的阈值和延迟时间间隔,控制角点显示的个数和协调不同设备的计算能力,当检测获取每帧图像的时间间隔差值达到阈值时,再检查Detector计算任务队列是否已经处理完毕,添加此帧图像进入计算队列让异步线程执行具体计算逻辑,最终将计算结果同步更新至渲染线程OpenGL数据缓存进行显示。
优化后的效果:
- IPhone 5S机型下实测:CPU 使用率稳定在50%左右, FPS稳定在35帧,GPU使用率为8%,整个页面进入新增内存在15M左右,卡顿现象已经彻底消失。
后续一些想法(欢迎一起探讨):
- 类似WebGL,将OpenGL渲染动态化,通过JS的语言动态特性解析执行OpenGL全链路渲染,开发OpenGL Native容器,以Native的性能动态执行一些JS脚本AR动效。
- 将此框架移植到Android,底层OpenGL层采用统一C语言实现,通过适配层提供统一接口和适配平台差异性。