Android相机OpenGL特效渲染

8,212 阅读8分钟

大家好,今天给大家介绍Android上如何利用OpenGL进行相机特效渲染。

相机特效渲染是什么呢?所谓特效是一个比较宽泛的概念,对相机采集到的画面做一定的修改,加上一定的效果再展示出来,都可以叫特效,比如我们有时候会用一些app来进行自拍,有美颜、提亮等效果,还可以在画面上添加各种贴纸,或者是让画面放大、缩小、抖动等,都是特效。

要实现相机特效渲染,总的来说有3步,首先需要采集到相机图像,然后对它进行特效处理,最后显示出来。

我们先来看看如果从相机采集数据,在Android上,相机有2种返回帧数据的方式,一种是以byte数组的方式返回,一种是以texture的方式返回。

前一种方式返回的byte数组可以直接在CPU上操作,处理后可转成bitmap最终显示到ImageView上,但这种方式效率相对来说比较低,因为对于图像处理和渲染,CPU远没有GPU效率高,但是这种方式学习门槛低,不需要学习OpenGL

后一种方式因为直接通过texture返回,因此从返回数据,到特效处理,到最后显示,全部可以通过OpenGLGPU上做,效率非常高,现在的商业app都是用这种方式,这篇文章也将介绍这种方式,如果你对OpenGL还不了解,可以参考我的Android OpenGL ES 2.0 手把手教学系列文章及OpenGL ES 高级进阶系列文章。

在使用相机时,首先需要打开相机,当然,相机权限必不可少,这里不哆嗦了,有很多文章讲解相机权限:

...
val cameraId = getCameraId(Camera.CameraInfo.CAMERA_FACING_BACK)
camera = Camera.open(cameraId)
...
private fun getCameraId(facing : Int) : Int {
    val numberOfCameras = Camera.getNumberOfCameras()
    for (i in 0 until numberOfCameras) {
        val info = Camera.CameraInfo()
        Camera.getCameraInfo(i, info)
        if (info.facing == facing) {
            return i
        }
    }
    return -1
}

首先需要获取要打开的相机的id,一般来说,手机上有前置和后置两个相机,这里获取了后置相机的id并打开了它,注意这里的打开并不是说相机就开始工作取景了,而仅仅得到这个相机对像而已,此时相机并没有开始工作,虽然没有开始工作,但如果不释放的话,别的应用再去打开会失败。

得到相机对象后,下面需要设置一些参数,有很多参数可以设置,如预览分辨率、对焦方式、拍照分辨率等等,这里我们只设置预览分辨率和显示角度。

相机的预览分辨率只能从支持的列表中选一个设置,不能设置任意值,实际使用时,一般会做些筛选逻辑,比如一个720P的屏幕,选了一个1080P的分辨率,其实没什么意义,会浪费资源,这里简单起见,我就直接取支持列表中的第0个。

private fun setPreviewSize(parameters: Camera.Parameters) {
    parameters.setPreviewSize(
        parameters.supportedPreviewSizes[0].width,					   
        parameters.supportedPreviewSizes[0].height
    )    
}

下面是设置旋转角度:

val info = Camera.CameraInfo()
Camera.getCameraInfo(cameraId, info)
camera.setDisplayOrientation(info.orientation)

这个旋转角度会影响到我们看到的图像的旋转,一般情况下这样设置就可以了,不过有些机型会有兼容性问题,需要特殊机型特殊设置。

前面说到,我们会让相机采集到的帧图像通过texture返回,这是通过setPreviewTexture方法设置的:

camera.setPreviewTexture(surfaceTexture)

注意这里的surfaceTexture它不是texturetexture是一个int类的值,这里的surfaceTextureSurfaceTexture类的一个对象,它是通过texture创建出来的,可以认为它是将texture包了一层,这里的texturesurfaceTexture都是我们自己创建的,然后设置给camera

既然要创建texture,那就需要OpenGL环境,关于OpenGL环境可以参考我的一篇文章《OpenGL ES 高级进阶:EGL及GL线程》,做OpenGL渲染,一般会用GLSurfaceView,它自带了OpenGL环境,不需要我们再去创建,这里我用TextureView,它是不带OpenGL环境的,我自己封装了一个OpenGL环境,使它的功能和GLSurfaceView一样,TextureView相比于GLSurfaceView来说还有一大好处就是,它同时有和普通view一样的功能,比如我们可以将它像一个普通的view一样放到一个RecyclerView中的item中显示,不会有问题,而如果把GLSurfaceView这样做,会有问题,类似的,如果你去移动一个GLSurfaceView,也会发现有这样那样的问题,根源就在于GLSurfaceView它不是在view树上的,它和普通的view是不一样的。因此,我这里封装了一个带有OpenGL环境的TextureView,叫GLTextureView,它比GLSurfaceView的功能更强大,而用于显示相机特效渲染的View继承了GLTextureView,叫GLCameraView,它可以绑定一个实现了ICamera接口的自定义camera类,用于向GLCameraView提供一些操作camera的方法以及一些获取心要信息的方法。

我们的texture也就是从这里的OpenGL环境创建出来的,注意这里创建的texture,不是普通的texture,是OES类型的texture,相机和视频硬解码出来的内容,都需要用OES类型的texture承载,否则会报错。

OES类型texture创建:

fun createOESTexture(): Int {
    val textures = IntArray(1)
    GLES30.glGenTextures(textures.size, textures, 0)
    GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0])
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_WRAP_S, 
        GLES30.GL_CLAMP_TO_EDGE
    )
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE
    )
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR
    )
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR
    )
    GLES30.glBindTexture(GLES30.GL_TEXTURE_EXTERNAL_OES, 0)
    return textures[0]
}

设置好SurfaceTexture之后,我们还需要给SurfaceTexture设置回调来感知相机给我们返回数据了:

...
st?.setOnFrameAvailableListener(this)
...
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
    ...
    surfaceTexture?.updateTexImage()
    ...
}

当相机给我们返回一帧数据时,onFrameAvailable就会回调一次告诉我们有数据了,注意这只是告诉我们有数据了,并不是告诉我们已经把数据放到texture上了,此时我们需要调用updateTexImage,相机帧数据才会更新到texture上,由于这一步是对texture进行操作,所以updateTexImage需要在GL线程调用。

onFrameAvailable是从GL线程回调过来吗?不一定,这处决于你在哪里创建SurfaceTexture,如果创建SurfaceTexture的线程有Looper,它就会持有这个Looper,之后回调onFrameAvailable就会通过这个Looper创建一个handler来在这个线程回调,如果创建SurfaceTexture的线程有没有Looper,它就会在主线程中回调过来。因此,如果创建SurfaceTexture的线程有Looper并且是GL线程,那么onFrameAvailable回调过来就是GL线程,否则就不是,此时就不能在onFrameAvailable里调用updateTexImage

在我的代码中,我确保onFrameAvailable回调过来就是GL线程,因此我直接在里面调用updateTexImage

这些都设置好之后,我们就调用startPreview,这时相机就真正开始工作了:

camera.startPreview()

此至,我们就完成了将相机帧数据采集到一个texture上,那么特效处理就以这个texture做为输入,利用OpenGL进行渲染,这里我使用我的一个库FunRenderer进行渲染,它将OpenGL进行了封装,使用起来很方便。

前面提到我们封装了一个GLCameraView用于显示渲染的结果,这里我通过callback回调3个方法,分别用于初始化、渲染、释放:

interface RenderCallback {

    fun onInit()
    fun onRenderFrame(oesTexture: Int, stMatrix: FloatArray, cameraPreviewSize: Size, surfaceSize: Size)
    fun onRelease()

}

我们也就是在这三个方法中使用FunRenderer

val cameraWrapper = CameraWrapper()
cameraView.bindCamera(cameraWrapper)
cameraView.renderCallback = object : GLCameraView.RenderCallback {

    private val oes2RGBARenderer = OES2RGBARenderer()
    private val cropRenderer = CropRenderer()
    private val effectRenderer = TestEffectRenderer()
    private val screenRenderer = ScreenRenderer()
    private lateinit var renderChain: RenderChain

    override fun onInit() {
        renderChain = RenderChain.create()
        .addRenderer(oes2RGBARenderer)
        .addRenderer(cropRenderer)
        .addRenderer(effectRenderer)
        .addRenderer(screenRenderer)
        renderChain.init()
    }

    override fun onRenderFrame(oesTexture: Int, stMatrix: FloatArray, cameraPreviewSize: Size, surfaceSize: Size) {
        GLES30.glClearColor(0f, 0f, 0f, 1f)
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        val input = Texture(oesTexture, cameraPreviewSize.height, cameraPreviewSize.width, false)
        val data = mutableMapOf<String, Any>()
        data[Keys.ST_MATRIX] = stMatrix
        data[Keys.CROP_RATIO] = surfaceSize.width.toFloat() / surfaceSize.height
        data[Keys.SURFACE_WIDTH] = surfaceSize.width
        data[Keys.SURFACE_HEIGHT] = surfaceSize.height
        renderChain.render(input, data)
    }

    override fun onRelease() {
        renderChain.release()
    }

}

这里每帧做的操作有OESRGBA、裁剪、一个简单的特效、上屏。

前面提到,相机返回的帧数据不是用普通的texture承载的,而是用OES类型的texture承载的,因此第一步需要通过OES2RGBARenderer将它转换成RGBAtexture

然后再通过CropRenderer进行一步裁剪,因为我们选的预览分辨率,可能和我们实现显示的区域的比例是不一样的,如果不裁剪,强行完全填充,就会变形。

然后我们做一个简单的特效,TestEffectRenderer继承于SimpleRenderer,我用一个简单的shader实现一个简单的特效:

#version 300 es
precision mediump float;
in vec2 v_textureCoordinate;
layout(location = 0) out vec4 fragColor;
uniform sampler2D u_texture;
void main() {
    vec4 c = texture(u_texture, v_textureCoordinate);
    c.b = 0.5;
    fragColor = c;
}

这里我简单地将蓝色通过的值设为0.5,得到的效果就是有点偏蓝,就像做了一个滤镜了一样。当然,实际使用的颜色效果滤镜比这个要复杂得多,一般用LUT实现,这里只是做一个简单的示例。

最后通过ScreenRenderer上屏,这样我们就完成了相机特效渲染,实际中大家看到的相机特效,无非就是自己写各种各样的shader,组合各种渲染步骤得到的。

我们来看下效果:

我封装了一个库HiCamera(github.com/kenneycode/…),可以方便地绑定相机进行特效渲染,demo里我用我的FunRenderer(github.com/kenneycode/…)库来渲染,可以自己继承FunRendererSimpleRenderer进行扩展,或者用别的,本文中的代码也都在HiCamerademo中。

感谢阅读!