课程介绍
本节课主要讲解如何通过OpenGL将相机获取到的内容渲染到画面上,而拍摄、录像、相机API则不是本节课的重点。
相机画面的渲染和视频的渲染都比较接近,OpenGL部分基本一致,主要是流程上需要对应处理下,所以,直接看本章节的朋友,可以先看下上一章Android OpenGL ES 10.1 视频播放器。
一. 视频播放器搭建
1. 视图容器
界面视图容器依旧使用GLSurfaceView,绘制方式是RENDERMODE_CONTINUOUSLY持续绘制的模式(课程演示,减少框架部分,相应的有不必要的性能损耗)。
2. 必要框架
因为本节涉及相机API、相机权限,该部分并非课程重点,因此详情见GLStudio工程代码。本课程中相机API参考自相机API。
①. 绑定纹理ID
// 1. 创建纹理ID
val textureIds = ......
// 2. 创建SurfaceTexture、Surface,并绑定到Camera上,接收画面驱动回调
surfaceTexture = SurfaceTexture(textureIds[0])
surfaceTexture!!.setOnFrameAvailableListener(this)
camera.setPreviewTexture(surfaceTexture)
这里分为以下几步:
- 创建纹理ID,用于GL渲染
- 通过纹理ID创建一个SurfaceTexture对象
- 将SurfaceTexture对象传入Camera中
通过以上3步,将纹理ID和Camera进行关联。
②. 接收画面解析完毕的回调
当Camera捕捉到新的画面时,Camera会通过层层的接口调用到SurfaceTexture的onFrameAvailable接口,这时候我们可以标志下画面已经解析完毕。
/**
* Camera有新的画面帧刷新时,通过SurfaceTexture的onFrameAvailable接口进行回调
*/
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
updateSurface = true
}
③. 驱动画面更新纹理ID
override fun onDrawFrame(glUnused: GL10) {
if (updateSurface) {
// 当有画面帧解析完毕时,驱动SurfaceTexture更新纹理ID到最近一帧解析完的画面,并且驱动底层去解析下一帧画面
surfaceTexture!!.updateTexImage()
updateSurface = false
}
// ...之后通过纹理ID绘制画面
}
在这里必须将updateTexImage放在onDrawFrame中进行调用,而不能放在第二步的onFrameAvailable方法中,因为这个方法内部介绍了,必须是在GL线程中进行调用,而不能在主线程中。而GL线程的生命周期方法有onSurfaceCreated、onSurfaceChanged、onDrawFrame,想要不停地更新画面,自然是放在onDrawFrame中最为合适。
到这里,可以发现整个流程和视频播放器基本一致。区别在Camera渲染容器传入的是SurfaceTexture,而MediaPlayer传入的是Surface,但二者根本原理是一致的,只是API封装了而已。
二. GL渲染相机画面
Shader代码部分,与视频部分完全一致。
之前课程中,我们渲染的纹理都是2D纹理,而在纹理绘制的课程中,有介绍到纹理单元是包括多种类型的纹理目标,包括了GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP、GL_TEXTURE_OES,而本节课用到了GL_TEXTURE_OES这个类型的纹理目标。
要启用GL_TEXTURE_OES这种拓展类型的纹理目标,需要对之前课程中的2D纹理进行以下修改。
1. 修改纹理类型声明
private const val FRAGMENT_SHADER = """
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 v_TexCoord;
uniform samplerExternalOES u_TextureUnit;
void main() {
gl_FragColor = texture2D(u_TextureUnit, v_TexCoord);
}
"""
在编写Shader时,需要声明当前使用的是拓展纹理GL_OES_EGL_image_external,然后采样器类型必须是samplerExternalOES。
另外经过测试,在Fragment Shader中,不能同时启用2D纹理和OES纹理的绘制,即使只是声明而不执行也是不可以的。
2. 创建纹理、绘制纹理
创建纹理、绘制纹理时,我们绑定的纹理目标类型必须改为是OES类型的。
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId)
在完成这一步时,应该就可以进行相机画面的播放了,不过还会有些小问题,比如比例问题,角度问题,前置摄像头翻转问题,下面会为大家解决这几个问题。
三. 解决相机角度问题
相机问题和上节课视频的一样,前置和后置摄像头都会自带一个方向角度(大部分都是后置90°,前置270°),导致预览到的画面和预期的不一样,这时候需要我们对画面方向进行处理。
问题效果图如下:
想要解决角度问题,可以通过旋转顶点坐标或旋转纹理坐标的方式去实现,这里我采用的是旋转顶点坐标的方式,工具类如下:
/**
* 顶点旋转工具类
*
* @author Benhero
* @date 2019/2/11
*/
object VertexRotationUtil {
enum class Rotation {
NORMAL, ROTATION_90, ROTATION_180, ROTATION_270;
}
fun getRotation(rotation: Int): Rotation {
return when (rotation) {
0 -> VertexRotationUtil.Rotation.NORMAL
90 -> VertexRotationUtil.Rotation.ROTATION_90
180 -> VertexRotationUtil.Rotation.ROTATION_180
270 -> VertexRotationUtil.Rotation.ROTATION_270
else -> VertexRotationUtil.Rotation.NORMAL
}
}
fun rotate(srcArray: FloatArray, rotation: Int): FloatArray {
return VertexRotationUtil.rotate(getRotation(rotation), srcArray)
}
fun rotate(rotation: VertexRotationUtil.Rotation, srcArray: FloatArray): FloatArray {
return when (rotation) {
VertexRotationUtil.Rotation.ROTATION_90 -> floatArrayOf(
srcArray[2], srcArray[3],
srcArray[4], srcArray[5],
srcArray[6], srcArray[7],
srcArray[0], srcArray[1])
VertexRotationUtil.Rotation.ROTATION_180 -> floatArrayOf(
srcArray[4], srcArray[5],
srcArray[6], srcArray[7],
srcArray[0], srcArray[1],
srcArray[2], srcArray[3])
VertexRotationUtil.Rotation.ROTATION_270 -> floatArrayOf(
srcArray[6], srcArray[7],
srcArray[0], srcArray[1],
srcArray[2], srcArray[3],
srcArray[4], srcArray[5])
else -> srcArray
}
}
fun flip(srcArray: FloatArray, isVertical: Boolean = false, isHorizontal: Boolean = false): FloatArray {
var temp = floatArrayOf(
srcArray[0], srcArray[1],
srcArray[2], srcArray[3],
srcArray[4], srcArray[5],
srcArray[6], srcArray[7])
temp = if (isVertical) floatArrayOf(
temp[2], temp[3],
temp[0], temp[1],
temp[6], temp[7],
temp[4], temp[5]) else temp
temp = if (isHorizontal) floatArrayOf(
temp[6], temp[7],
temp[4], temp[5],
temp[2], temp[3],
temp[0], temp[1]) else temp
return temp
}
}
问题解决后效果如下图:
三. 解决前期相机垂直翻转问题
在使用前置相机预览的时候,会发现画面有垂直翻转的问题,画面随着Y轴中间左右调换了,导致画面左边对应到现实的右边。对此,只要将顶点坐标左右互换即可,使用到的是上面工具类的flip方法进行处理。
四. 摄像头切换的处理
切换摄像头的时候,由于输入源Camera对象变了,已经不是同个对象,所以这时候Renderer渲染类需要重新创建纹理ID,并且更新顶点坐标。
具体是在onDrawFrame方法中,去处理一次。
override fun onDrawFrame(glUnused: GL10) {
if (isSwitchCamera) {
isSwitchCamera = false
createOESTextureId()
updateVertex()
}
}
/**
* 根据相机方向更新顶点坐标
*/
private fun updateVertex() {
var array = VertexRotationUtil.rotate(POINT_DATA, camera.rotation)
array = VertexRotationUtil.flip(array, camera.isFront)
mVertexData = BufferUtil.createFloatBuffer(array)
mVertexData.position(0)
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
GLES20.GL_FLOAT, false, 0, mVertexData)
GLES20.glEnableVertexAttribArray(aPositionLocation)
}
其他
- 本系列课程目录详见 - Android OpenGL ES教程规划
- 参考资料见Android OpenGL ES学习资料所列举的博客、资料。
- Camera API参考Android OpenGL ES(五)-结合相机进行预览/录制及添加滤镜
⭐GitHub工程⭐
本系列课程所有相关代码请参考我的GitHub项目⭐GLStudio⭐,喜欢的请给个小星星。