GLSurfaceView,SurfaceView,TextureView跟OpenGL,OpenGL ES关系,前面有整理过一篇:Android UI OpenGL初识,SurfaceView,GLSurfaceView 和 Renderer
本篇主要通过实现:GLSurfaceView
预览摄像头数据,并且对摄像头数据进行简单再处理
来熟悉GLSurfaceView使用
1.
GLSurfaceView基本用法
1.1
GLSurfaceView创建
// 1. 创建GLSurfaceView
glSurfaceView = new GLSurfaceView(this);
// 2. GLContext设置OpenGLES2.0
glSurfaceView.setEGLContextClientVersion(2);
// 3. 指定自定义渲染器
glSurfaceView.setRenderer(new MyRender());
// 4. 渲染方式:提供连续渲染或按需渲染能力
// 1. RENDERMODE_WHEN_DIRTY表示被动渲染 只有在调用 requestRender 或者 onResume 等方法时才会进行渲染
// 2. RENDERMODE_CONTINUOUSLY表示持续渲染
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
GLSurfaceView主要包括以下能力:
- 提供一个OpenGL的渲染线程,以防止渲染阻塞主线程。
- 提供连续渲染或按需渲染能力。
- 封装EGL相关资源和创建和释放,极大地简化了OpenGL与窗口系统接口的使用方式。
1.2
获取摄像头数据
获取摄像头数据有一般有两种方式
- 相机设置预览的
SurfaceTexture
,通过回调获得当前可用的摄像头纹理
- 相机设置Camera.PreviewCallback回调,通过回调拿到YUV数据。YUV数据格式默认为
NV21
,也可以通过parameter.setPreviewFormat(ImageFormat format)来指定YUV数据格式
。一般来说,NV21和YV12两种格式是所有Android机型都支持的,其他格式可能在不同机型上有兼容性问题
要对摄像头数据做再处理,首先要拿到摄像头数据(这里我们采用第一种方式获取)
1.2.1
开启摄像头
private void startCamera() {
if (mCamera != null) {
return;
}
Camera.CameraInfo info = new Camera.CameraInfo();
// Try to find a front-facing camera
int numCameras = Camera.getNumberOfCameras();
// Log.i(TAG, "startCamera numCameras " + numCameras);
int i;
for (i = 0; i < numCameras; i++) {
Camera.getCameraInfo(i, info);
// 遍历摄像头,默认设置为前置摄像头ID
int cameraFacing = Camera.CameraInfo.CAMERA_FACING_FRONT;
if (info.facing == cameraFacing) {
mCamera = Camera.open(i);
break;
}
}
if (mCamera == null) {
throw new RuntimeException("Unable to open camera");
}
Camera.Parameters params = mCamera.getParameters();
// 设置预览尺寸
// 获取摄像头支持的PreviewSize列表
List<Camera.Size> previewSizeList = params.getSupportedPreviewSizes();
for (Camera.Size size : previewSizeList) {
Log.i(TAG, "previewSizeList width: " + size.width + " height=" + size.height);
}
Camera.Size preSize = getCloselyPreSize(glSurfaceView.getWidth(), glSurfaceView.getHeight(), previewSizeList);
if (null != preSize) {
params.setPreviewSize(preSize.width, preSize.height);
}
mCamera.setParameters(params);
}
因为采用GLSurfaceView预览,为避免画面变形,需要筛选最合适的相机预览尺寸,通过上述1中getCloselyPreSize
方法,具体代码查看 Android Camera 从0到1,文章中有现成代码段实现了通过预览GLSurfaceView宽高比筛选合适的预览宽高。
1.2.2
相机设置预览的SurfaceTexture
这个只需要创建SurfaceTexture
设置给Camera预览就好了
设置之后,相机会源源不断地把摄像头帧数据更新到SurfaceTexture上,即更新到对应的OpenGL纹理上。但是此时我们并不知道相机数据帧何时会更新到
SurfaceTexture,也没有在GLSurfaceView的OnDrawFrame方法中将更新后的纹理渲染到屏幕,所以并不能在屏幕上看到预览画面
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清除画布
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
mSurfaceTexture = new SurfaceTexture(createOESTextureObject());
createProgram();
startCamera();
// mCamera = Camera.open(camera_status);
try {
mCamera.setPreviewTexture(mSurfaceTexture);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
activeProgram();
}
1.3
设置SurfaceTexture回调,通知摄像头预览数据已更新
SurfaceTexture有一个很重要的回调:OnFrameAvailableListener
。通过名字也可以看出该回调的调用时机,当相机有新的预览帧数据
时,此回调会被调用
。所以我们为前面的SurfaceTexture设置一个回调,来通知我们相机预览数据已更新
SurfaceTexture的updateTexImage
方法会更新接收到的预览数据到其绑定的OpenGL纹理
中,接下来利用OpenGL
对相机流数据进行处理(顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)),以相机纹理
和变换矩阵
作为输入
,把相机流数据
渲染在GSurfaceView
上
1.4
编写及初始化OpenGL着色器程序
代码段如下,主要是实现GLSurfaceView.Renderer
接口,实现OpenGL ES程序
对象创建,然后通过顶点着色器
和片段着色器
来对步骤2中获取的纹理进行绘制,处理操作
class MyRender implements GLSurfaceView.Renderer {
// -------------------------------------------------------------------------
// 顶点着色器代码
private final String vertexShaderCode = "uniform mat4 textureTransform;\n" +
"attribute vec2 inputTextureCoordinate;\n" +
"attribute vec4 position; \n" +//NDK坐标点
"varying vec2 textureCoordinate; \n" +//纹理坐标点变换后输出
"\n" +
" void main() {\n" +
" gl_Position = position;\n" +
" textureCoordinate = inputTextureCoordinate;\n" +
" }";
// 片段着色器
private final String fragmentShaderCode = "#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"uniform samplerExternalOES videoTex;\n" +
"varying vec2 textureCoordinate;\n" +
"\n" +
"void main() {\n" +
" vec4 tc = texture2D(videoTex, textureCoordinate);\n" +
" float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;\n" + //所有视图修改成黑白
" gl_FragColor = vec4(color,color,color,1.0);\n" +
// " gl_FragColor = vec4(tc.r,tc.g,tc.b,1.0);\n" +
"}\n";
// OpenGL ES程序
public int mProgram;
private float[] mProjectMatrix = new float[16];
// 视见转换矩阵
private float[] mCameraMatrix = new float[16];
private float[] mTempMatrix = new float[16];
public MyRender() {
Matrix.setIdentityM(mProjectMatrix, 0);
Matrix.setIdentityM(mCameraMatrix, 0);
Matrix.setIdentityM(mMVPMatrix, 0);
Matrix.setIdentityM(mTempMatrix, 0);
}
// -------------------------------------------------------------------------
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清除画布
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
mSurfaceTexture = new SurfaceTexture(createOESTextureObject());
createProgram();
startCamera();
// mCamera = Camera.open(camera_status);
try {
mCamera.setPreviewTexture(mSurfaceTexture);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
activeProgram();
}
// 投影矩阵:主要进行 3D 及 NDC 坐标变换
private float[] mMVPMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.i(TAG, "onSurfaceChanged width:" + width + " | height:" + height);
GLES20.glViewport(0, 0, width, height);
// Matrix.scaleM(mMVPMatrix, 0, 1, -1, 1);
// float ratio = (float) width / height;
// Log.i(TAG, "onSurfaceChanged ratio:" + ratio);
// 正交投影方法 (3 和 7 代表远近视点与眼睛的距离,非坐标点)
// Matrix.orthoM(mProjectMatrix, 0, -1, 1, -ratio, ratio, 1, 7);
// Android 使用 OpenGL ES2.0 绘制3D图像或者加载3D模型时,为了达到立体效果往往需要设置视见转换矩阵
// 3代表眼睛的坐标点 (图片旋转等需要调节此方法相关参数)
// Matrix.setLookAtM(mCameraMatrix, 0, 0, 0, 3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Matrix.multiplyMM(mMVPMatrix, 0, mProjectMatrix, 0, mCameraMatrix, 0);
}
public boolean mBoolean = false;
@Override
public void onDrawFrame(GL10 gl) {
if (mBoolean) {
activeProgram();
mBoolean = false;
}
if (mSurfaceTexture != null) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
mSurfaceTexture.updateTexImage();
// GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
// 以 GL_TRIANGLE_STRIP 方式渲染传入的 (mPosCoordinate.length / 2) 个顶点数据
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mPosCoordinate.length / 2);
}
}
// -------------------------------------------------------------------------
public int createOESTextureObject() {
int[] tex = new int[1];
//生成一个纹理
GLES20.glGenTextures(1, tex, 0);
//将此纹理绑定到外部纹理上
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0]);
//设置纹理过滤参数
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
return tex[0];
}
// -------------------------------------------------------------------------
/**
* 有了顶点着色器和片段着色器程序, 把它们加在OpenGL渲染管线中运行起来
*
* OpenGL着色器程序和普通程序的运行准备过程差不多,也需要通过编译和链接后才可使用
*/
private void createProgram() {
//通常做法
// String vertexSource = AssetsUtils.read(CameraGlSurfaceShowActivity.this, "vertex_texture.glsl");
// int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
// String fragmentSource = AssetsUtils.read(CameraGlSurfaceShowActivity.this, "fragment_texture.glsl");
// int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
// 1. 创建,加载并编译顶点着色器,
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
// 2. 创建,加载并编译片段着色器,
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
// 3. 创建一个 空的OpenGL ES程序
mProgram = GLES20.glCreateProgram();
// 4. 添加顶点着色器到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 5. 添加片段着色器到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
// 6. 链接OpenGL ES程序(创建OpenGL ES程序可执行文件)
GLES20.glLinkProgram(mProgram);
// 释放shader资源
GLES20.glDeleteShader(vertexShader);
GLES20.glDeleteShader(fragmentShader);
}
/**
* 着色器中有三种类型的参数:uniform、attribute和varying。
* varying参数是顶点着色器和片段着色器之前传递参数用的,对外部程序不可见,
* 所以外部程序能传入着色器的参数只有 uniform 和 attribute 类型
*
* @param type
* @param shaderCode
* @return 创建并加载着色器代码
*/
private int loadShader(int type, String shaderCode) {
// 创建指定类型的着色器
int shader = GLES20.glCreateShader(type);
// 加载着色器代码(添加上面编写的着色器代码并编译它)
GLES20.glShaderSource(shader, shaderCode);
// 编译着色器
GLES20.glCompileShader(shader);
return shader;
}
private float[] mPosCoordinate = {-1, -1, -1, 1, 1, -1, 1, 1};
private FloatBuffer mPosBuffer;
// 顺时针转90并沿Y轴翻转 后摄像头正确,前摄像头上下颠倒
private float[] mTexCoordinateBackRight = {1, 1, 0, 1, 1, 0, 0, 0};
// 顺时针旋转90 后摄像头上下颠倒了,前摄像头正确
private float[] mTexCoordinateForntRight = {0, 1, 1, 1, 0, 0, 1, 0};
private FloatBuffer mTexBuffer;
private int uPosHandle;
private int aTexHandle;
private int mMVPMatrixHandle;
/**
* 添加程序到ES环境中
*/
private void activeProgram() {
// 将程序添加到OpenGL ES环境,激活程序对象
// 在 glUseProgram 函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了
GLES20.glUseProgram(mProgram);
mSurfaceTexture.setOnFrameAvailableListener(SurfaceAct.this);
// attribute 类型参数都需要用glGetAttribLocation获取句柄
// 获取顶点着色器的位置的句柄
uPosHandle = GLES20.glGetAttribLocation(mProgram, "position");
aTexHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
// uniform 参数则是用 glGetUniformLocation 获取句柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "textureTransform");
mPosBuffer = convertToFloatBuffer(mPosCoordinate);
if (camera_status == 0) {
mTexBuffer = convertToFloatBuffer(mTexCoordinateBackRight);
} else {
mTexBuffer = convertToFloatBuffer(mTexCoordinateForntRight);
}
// glVertexAttribPointer 或 VBO 只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU
// 1. 第一个参数指定句柄
// 2. 第二个参数指定顶点属性的大小,每个坐标点包含x和y两个float值
// 3. 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec都是由浮点数值组成的)
// 4. 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间,
// 这里我们把它设置为GL_FALSE
// 5. 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔,由于下个组位置数据在2个GLfloat之后,我们把步长设置为2 sizeof(GLfloat)
GLES20.glVertexAttribPointer(uPosHandle, 2, GLES20.GL_FLOAT, false, 0, mPosBuffer);
GLES20.glVertexAttribPointer(aTexHandle, 2, GLES20.GL_FLOAT, false, 0, mTexBuffer);
// 启用顶点位置的句柄
// 着色器能否读取到数据 由下面的 glEnableVertexAttribArray 调用来决定
// glEnableVertexAttribArray 调用后允许顶点着色器读取句柄对应的GPU数据
GLES20.glEnableVertexAttribArray(uPosHandle);
GLES20.glEnableVertexAttribArray(aTexHandle);
}
private FloatBuffer convertToFloatBuffer(float[] buffer) {
FloatBuffer fb = ByteBuffer.allocateDirect(buffer.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
fb.put(buffer);
fb.position(0);
return fb;
}
// -------------------------------------------------------------------------
}
简单走完上面4步,总体GLSurfaceView用法应该是完整的(步骤4中代码很长,注释也写了一些,可以拷贝到项目中运行看一下效果)
2.
SurfaceView使用常见问题
2.1
Fragment显示SurfaceView黑屏
// 给窗口Window设置
getWindow().setFormat(PixelFormat.TRANSLUCENT);
// SurfaceHolder设置
getHolder().setFormat(PixelFormat.TRANSPARENT);
2.2
解决SurfaceView调用setZOrderOnTop(true)遮挡其他控件的问题
//
...
setZOrderOnTop(true)
//
...
setZOrderMediaOverlay(true)
2.3
SurfaceView绘图表面的类型
// 设置类型(设置SurfaceView显示类型,已经标记为过时)
// 当一个SurfaceView的绘图表面的类型等于SURFACE_TYPE_NORMAL的时候,就表示该SurfaceView的绘图表面所使用的内存是一块普通的内存
// 通常情况下,当一个SurfaceView是用来显示摄像头预览或者视频播放的时候,我们就会将它的绘图表面的类型设置为SURFACE_TYPE_PUSH_BUFFERS
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
实话说
我也还有很多代码不懂(OpenGL不熟悉),后面我会继续完善,文末的链接大家真心想学好OpenGL或者想熟悉它的,可以好好花半小时看两遍
最后还是简单总结一下:
- GLSurfaceView让我们自定义录制视频源有了可能(直播中美颜,滤镜等)
- SurfaceTexture很好的解决了视频源只获取不展示的功能,有点类似Bitmap是否加载到内存的那个isJustBu…的方法
- GLSurfaceView渲染方式:提供连续渲染或按需渲染能力
6.
参考链接