大家好,今天给大家介绍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
中。
感谢阅读!