实现的基本功能
- 使用GLSurfaceView绘制camera的preview内容。
- 支持前后摄像头切换。
- 支持切换preview size。
- 通过手势可以缩放preview画面,移动previw画面。
初识OpenGl
GLSurfaceView为我们构建了一个OpenGl环境,如果我们想通过GLSurfaceView来渲染camera的Preview内容,那么我们必须掌握一些基础的OpenGl相关知识。如何使用OpenGL es接口绘制图形。重点需要学习下面的知识内容:
- 定义纹理显示位置,在没有使用mvp矩阵的情况下x轴、y轴、z轴的范围都是-1~1。
- 2d 纹理的栅格化的x轴、y轴范围是0~1
- 简单了解mvp矩阵的作用,因为opengl的位置信息与view系统的位置信息不统一,所以我们可以考虑使用mvp矩阵来把view系统的位置信息变换成opengl的位置信息。
- 如何使用纹理创建surface,因为camera的preview内容可以输出到surface上。
具体的代码实现
为了能够在GLSurfaceView上绘制内容,我定义了一个CustomRender类,这个类实现了GLSurfaceView.Renderer接口。我们可以在onDrawFrame方法中使用OPenGl来绘制camera preview内容。通常情况下我们除了需要绘制camera的preview内容,我们还需要绘制水印,sticker,filter等内容。所以我在这里把每一个绘制内容都抽象成一个node。NodeRender就是用于管理这些内容的。我们可以根据具体的需求来动态的添加绘制内容。frontBuffer是一个framebuffer的texture,NodeRender会将所有的node内容绘制到这个framebuffer上。
override fun onDrawFrame(gl: GL10?) {
//绘制所以的node到frontBuffer上
val frontBuffer = nodesRender.render()
//清除屏幕内容
GLES20.glViewport(0, 0, width, height)
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
//初识话显示用的Opengl程序
if (displayProgram == null) {
displayProgram = TextureProgram()
}
frontBuffer?:return
//对显示位置进行变换,实现preview内容的缩放、移动操作。
matrixHandler.applyVertexMatrix(OpenGlUtils.BufferUtils.SQUARE_VERTICES, displayPosition)
//进行描画
displayProgram!!.draw(
frontBuffer.textureId,//描画的texture
displayPosition,//描画的位置信息
displayTextureCoordinate,//描画的纹理范围信息
matrixValues//这里没有进行位置变换,所以使用的是单位矩阵
)
}
TextureProgram中的opengl程序是什么样的呢?首先来看下它定义的vertex shader和fragment shader。这两个shader非常简单,唯一需要注意的是位置坐标会经过mvp矩阵变换。我们这里传递的是单位矩阵,相当于无转换。如果我们的位置信息是参照view系统的而不是opengl系统定义的范围,那么我们需要根据具体的viewport大小来设置mvp矩阵。mvp矩阵设置方法。
"""
attribute vec2 vPosition;
attribute vec2 vInputTextureCoordinate;
uniform mat4 mvpMatrix;
varying vec2 vTextureCoordinate;
void main(){
gl_Position = mvpMatrix * vec4(vPosition,0.0,1.0);
vTextureCoordinate = vInputTextureCoordinate;
}
""",
"""
precision mediump float;
uniform sampler2D inputTexture;
varying vec2 vTextureCoordinate;
void main(){
gl_FragColor = texture2D(inputTexture, vTextureCoordinate);
}
"""
matrixHandler是如何实现的缩放和移动处理的呢?这里通过对显示矩形进行缩放和移动来实现的。显示矩形的计算主要通过updateVertexMatrix方法实现的。这个方法的输入参数包括屏幕的宽高和显示texture的宽高。通过输入的参数可以计算出fix center的显示矩形和fill center的显示矩形。我们可以根据实际需求来选择使用哪种显示矩形。详细的计算代码请参照下面的代码段。
protected void updateVertexMatrix(int screenWidth, int screenHeight, int sourceWidth, int sourceHeight) {
if (screenWidth <= 0 || screenHeight <= 0 || sourceWidth <= 0 || sourceHeight <= 0) {
return;
}
boolean isLandscape = sourceRotate % 180 != 0;
screenRectF = new RectF(0, 0, screenWidth, screenHeight);
sourceRectF = new RectF(0, 0, isLandscape ? sourceHeight : sourceWidth, isLandscape ? sourceWidth : sourceHeight);
if (displayRectF == null) {
displayRectF = new RectF(0, 0, screenWidth, screenHeight);
}
minimumScaleSourceRectF = new RectF();
maximumScaleSourceRectF = new RectF();
float scaleH = displayRectF.height() / sourceRectF.height();
float scaleW = displayRectF.width() / sourceRectF.width();
minimumRealScale = scaleH < scaleW ? scaleH : scaleW;
maximumRealScale = scaleH > scaleW ? scaleH : scaleW;
Matrix matrix = new Matrix();
matrix.setTranslate(displayRectF.centerX() - sourceRectF.centerX(), displayRectF.centerY() - sourceRectF.centerY());
matrix.postScale(minimumRealScale, minimumRealScale, displayRectF.centerX(), displayRectF.centerY());
matrix.mapRect(minimumScaleSourceRectF, sourceRectF);
matrix.reset();
matrix.setTranslate(displayRectF.centerX() - sourceRectF.centerX(), displayRectF.centerY() - sourceRectF.centerY());
matrix.postScale(maximumRealScale, maximumRealScale, displayRectF.centerX(), displayRectF.centerY());
matrix.mapRect(maximumScaleSourceRectF, sourceRectF);
currentScaleRectF = new RectF(minimumScaleSourceRectF);
currentScale = minimumRealScale;
initRectFlag = true;
updateMatrix();
}
显示矩形已经计算好了,下面需要更新vertex的变换矩阵,使显示矩形的位置能够正确的映射到opengl的position进行显示位置的控制。这里通过updateMatrix()方法来更新变换矩阵。然后我们就可以对vertext坐标应用applyVertexMatrix来控制显示位置。在发生缩放或是移动手势的时候,我们对currentScaleRectF矩形实施缩放和移动操作,然后在调用updateMatrix()更新变换矩阵。
private void updateMatrix() {
vertexMatrixLock.lock();
try {
float scaleX = currentScaleRectF.width() / screenRectF.width();
float scaleY = currentScaleRectF.height() / screenRectF.height();
applyVertexMatrix.reset();
applyVertexMatrix.setScale(scaleX, scaleY);
applyVertexMatrix.postTranslate((currentScaleRectF.centerX() - screenRectF.centerX()) * 2 / screenRectF.width(), -(currentScaleRectF.centerY() - screenRectF.centerY()) * 2 / screenRectF.height());
needUpdateVertexMatrix = true;
} finally {
vertexMatrixLock.unlock();
}
}
由于camera需要设置surface来接收录制的内容,所以我们来看下如何通过textureId来生成surface。这里我定义了一个CombineSurfaceTexture类用于封装surface类型的texture。首先生成一个textureId,然后使用textureId生成一个SurfaceTexture对象,再用SurfaceTexture生成Surface对象。我这里使用GLSurfaceView.RENDERMODE_WHEN_DIRTY方式更新,所以在OnFrameAvailableListener中需要通知GLSurfaceView来更新。在我们绘制这个textureId的内容时,我们需要主动调用surfaceTexture.updateTexImage()来刷新到最新的数据。
class CombineSurfaceTexture(
width: Int,
height: Int,
orientation: Int,
flipX: Boolean = false,
flipY: Boolean = false,
notify: () -> Unit = {}
) :
BasicTexture(width, height, orientation, flipX, flipY) {
private val surfaceTexture: SurfaceTexture
val surface: Surface
init {
textureId = OpenGlUtils.createTexture()
surfaceTexture = SurfaceTexture(textureId)
surfaceTexture.setDefaultBufferSize(width, height)
surfaceTexture.setOnFrameAvailableListener { notify.invoke() }
surface = Surface(surfaceTexture)
}
fun update() {
if (surface.isValid) {
surfaceTexture.updateTexImage()
}
}
override fun release() {
super.release()
surface.release()
surfaceTexture.release()
}
}
在绘制Surface类型的texture的时候,我们需要声明输入texture的类型为samplerExternalOES。
"""
attribute vec2 vPosition;
attribute vec2 vInputTextureCoordinate;
uniform mat4 mvpMatrix;
varying vec2 vTextureCoordinate;
void main(){
gl_Position = mvpMatrix * vec4(vPosition,0.0,1.0);
vTextureCoordinate = vInputTextureCoordinate;
}
""",
"""
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES inputTexture;
varying vec2 vTextureCoordinate;
void main(){
gl_FragColor = texture2D(inputTexture, vTextureCoordinate);
}
"""
这里没有直接把surface类型的texture绘制到GLSurfaceView上,而是先将它绘制到frameBuffer上然后再绘制到GLSurfaceView。这是因为为后面实现录制、添加filter、添加水印等工作做准备。
前后摄像头切换和更改分辨率的操作是什么样的处理呢?其实从下面的代码可以看到这两种情况下都把preview使用的texture释放了并生成新的texture和surface。由于我这里把绘制的程序封装到node里,所以这里进行了preview node替换。
switchCamera.setOnClickListener {
nodesRender.runInRender {
val cameraId = when (cameraHolder.cameraId) {
CAMERA_REAR -> CAMERA_FRONT
else -> CAMERA_REAR
}
cameraHolder.cameraId = cameraId
updatePreviewNode(
cameraHolder.previewSizes.first().width,
cameraHolder.previewSizes.first().height
)
cameraHolder.setSurface(cameraPreviewNode!!.combineSurfaceTexture.surface)
.invalidate()
}
}
previewSize.setOnClickListener {
cameraHolder?.let {
it.previewSizes?.let { sizes ->
val builder = AlertDialog.Builder(this@MainActivity)
val sizesString: Array<String> = Array(sizes?.size ?: 0) { "" }
sizes?.forEachIndexed { index, item ->
sizesString[index] = item.width.toString() + "*" + item.height.toString()
}
builder.setItems(sizesString) { d, index ->
val size = sizesString[index].split("*")
val width = size[0].toInt()
val height = size[1].toInt()
nodesRender.runInRender {
updatePreviewNode(width, height)
cameraHolder.setSurface(cameraPreviewNode!!.combineSurfaceTexture.surface)
.invalidate()
}
}
builder.create().show()
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。