大家好,今天给大家介绍Android上如何利用OpenGL进行相机特效渲染。
相机特效渲染是什么呢?所谓特效是一个比较宽泛的概念,对相机采集到的画面做一定的修改,加上一定的效果再展示出来,都可以叫特效,比如我们有时候会用一些app来进行自拍,有美颜、提亮等效果,还可以在画面上添加各种贴纸,或者是让画面放大、缩小、抖动等,都是特效。
要实现相机特效渲染,总的来说有3步,首先需要采集到相机图像,然后对它进行特效处理,最后显示出来。
我们先来看看如果从相机采集数据,在Android上,相机有2种返回帧数据的方式,一种是以byte数组的方式返回,一种是以texture的方式返回。
前一种方式返回的byte数组可以直接在CPU上操作,处理后可转成bitmap最终显示到ImageView上,但这种方式效率相对来说比较低,因为对于图像处理和渲染,CPU远没有GPU效率高,但是这种方式学习门槛低,不需要学习OpenGL。
后一种方式因为直接通过texture返回,因此从返回数据,到特效处理,到最后显示,全部可以通过OpenGL在GPU上做,效率非常高,现在的商业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它不是texture,texture是一个int类的值,这里的surfaceTexture是SurfaceTexture类的一个对象,它是通过texture创建出来的,可以认为它是将texture包了一层,这里的texture和surfaceTexture都是我们自己创建的,然后设置给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()
}
}
这里每帧做的操作有OES转RGBA、裁剪、一个简单的特效、上屏。
前面提到,相机返回的帧数据不是用普通的texture承载的,而是用OES类型的texture承载的,因此第一步需要通过OES2RGBARenderer将它转换成RGBA的texture。
然后再通过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/…)库来渲染,可以自己继承FunRenderer的SimpleRenderer进行扩展,或者用别的,本文中的代码也都在HiCamera的demo中。
感谢阅读!