话不多说,今天我们通过撸代码来更进一步理解opengl的使用。
1.新建一个AS项目
名字自定义,我的项目名叫 XOpenGLCamera,包名是 com.xopengl.org。为了方便大家理解,项目里大部分使用的是java语言编写,其实本人更喜欢kotlin。
新建的项目结构,这个没什么好说的。
2.编写代码前的准备
1.添加权限
在项目的AndroidManifest.xml文件中添加:
2.添加第三方库
由于android5.0以上的动态权限机制,我们需要动态申请权限,这里使用一个第三方库,RxPermission,想具体了解的可以看这篇文章:RxPermission动态权限申请
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.0.1'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'com.github.tbruyelle:rxpermissions:0.10.2'
}
3.开始撸代码(这里我们按照思路来写,最终代码可以参考我的git项目)
要使用opengl,就要用到 GLSurfaceView,我们再回忆一下 GLSurfaceView类的使用流程:
1. GLSurfaceView中
setEGLContextClientVersion(2); //设置opengl使用的版本号
setRenderer( renderer ); // 重点: 设置renderer
setRenderMode(RENDERMODE_WHEN_DIRTY);// 设置刷新模式,一般都是按需刷新
2. renderer: 实现Renderer接口
// GLSurfaceView 画布刚刚创建好的时候调用 可以在这里打开相机
public void onSurfaceCreated(GL10 gl, EGLConfig config);
// 屏幕画布发生变化的时候 可以在这里打开相机预览
public void onSurfaceChanged(GL10 gl, int width, int height);
// 具体绘制的方法
public void onDrawFrame(GL10 gl) ;
3. 实现一个 filter类,用于处理相机的预览数据,调用opengl方法生成纹理并显示在屏幕上等等
所以重点的代码是在renderer中,在renderer之前,我们要先创建好相机管理的工具类。
1.CameraHelper 相机管理工具类
这里使用的是 android.hardware.Camera,这里不做多的说明了,总体要 用到的就是开始预览和关闭预览的方法
import android.graphics.ImageFormat;
import android.graphics.Point;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class CameraHelper implements Camera.PreviewCallback {
private static final String TAG = "CameraHelper";
public int WIDTH = 720;
public int HEIGHT = 1280;
private int mCameraId;
private Camera mCamera;
private byte[] buffer;
private Camera.PreviewCallback mPreviewCallback;
private SurfaceTexture mSurfaceTexture;
/**
* 额外的旋转角度(用于适配一些定制设备)
*/
private int additionalRotation = 0;
public CameraHelper(int cameraId) {
mCameraId = cameraId;
}
public void switchCamera() {
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
} else {
mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
}
stopPreview();
startPreview(mSurfaceTexture);
}
public int getCameraId() {
return mCameraId;
}
public void stopPreview() {
if (mCamera != null) {
//预览数据回调接口
mCamera.setPreviewCallback(null);
//停止预览
mCamera.stopPreview();
//释放摄像头
mCamera.release();
mCamera = null;
}
}
public void startPreview(SurfaceTexture surfaceTexture) {
mSurfaceTexture = surfaceTexture;
try {
//获得camera对象
mCamera = Camera.open(mCameraId);
//配置camera的属性
Camera.Parameters parameters = mCamera.getParameters();
//设置预览数据格式为nv21
parameters.setPreviewFormat(ImageFormat.NV21);
//预览大小设置
Camera.Size previewSize = parameters.getPreviewSize();
List supportedPreviewSizes = parameters.getSupportedPreviewSizes();
if (supportedPreviewSizes != null && supportedPreviewSizes.size() > 0) {
previewSize = getBestSupportedSize(supportedPreviewSizes, new Point(WIDTH,HEIGHT));
}
parameters.setPreviewSize(previewSize.width, previewSize.height);
//对焦模式设置
List supportedFocusModes = parameters.getSupportedFocusModes();
if (supportedFocusModes != null && supportedFocusModes.size() > 0) {
if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
} else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
}
}
// 设置摄像头 图像传感器的角度、方向
mCamera.setParameters(parameters);
buffer = new byte[previewSize.width * previewSize.height * 3 / 2];
//数据缓存区
mCamera.addCallbackBuffer(buffer);
mCamera.setPreviewCallbackWithBuffer(this);
//设置预览画面
mCamera.setPreviewTexture(mSurfaceTexture);
mCamera.startPreview();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void setPreviewCallback(Camera.PreviewCallback previewCallback) {
mPreviewCallback = previewCallback;
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
// data数据依然是倒的
if (null != mPreviewCallback) {
mPreviewCallback.onPreviewFrame(data, camera);
}
camera.addCallbackBuffer(buffer);
}
private Camera.Size getBestSupportedSize(List sizes, Point previewViewSize) {
if (sizes == null || sizes.size() == 0) {
return mCamera.getParameters().getPreviewSize();
}
Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]);
Arrays.sort(tempSizes, new Comparator() {
@Override
public int compare(Camera.Size o1, Camera.Size o2) {
if (o1.width > o2.width) {
return -1;
} else if (o1.width == o2.width) {
return o1.height > o2.height ? -1 : 1;
} else {
return 1;
}
}
});
sizes = Arrays.asList(tempSizes);
Camera.Size bestSize = sizes.get(0);
float previewViewRatio;
if (previewViewSize != null) {
previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y;
} else {
previewViewRatio = (float) bestSize.width / (float) bestSize.height;
}
if (previewViewRatio > 1) {
previewViewRatio = 1 / previewViewRatio;
}
boolean isNormalRotate = (additionalRotation % 180 == 0);
for (Camera.Size s : sizes) {
if (WIDTH == s.width && HEIGHT == s.height) {
return s;
}
if (isNormalRotate) {
if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) {
bestSize = s;
}
} else {
if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) {
bestSize = s;
}
}
}
return bestSize;
}
}
2.MyCameraRenderer
自定义的 Renderer
1.onSurfaceCreated
//打开前置摄像头
cameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_FRONT);
// GLES20 创建一个SurfaceTexture id 数组,把生成的 SurfaceTexture id放入数组中,方便我们后面使用
mTextures = new int[1];
GLES20.glGenTextures(mTextures.length,mTextures,0);
// 生成一个 SurfaceTexture
surfaceTexture = new SurfaceTexture(mTextures[0]);
// 给这个 surfaceTexture 设置监听
surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// 当页面获取到新的有效的数据的时候会调用到这个回调
// GLSurfaceView 的 requestRender()方法,会触发 GLSurfaceView 的render的 onDrawFrame方法
glSurfaceView.requestRender();
}
});
// 生成相机数据处理类
screenFilter = new ScreenFilter(glSurfaceView.getContext());
2.onSurfaceChanged
// 打开手机摄像头预览功能
cameraHelper.WIDTH = width;
cameraHelper.HEIGHT = height;
cameraHelper.startPreview(surfaceTexture);
// 设置相机数据处理工具类中的 宽高
screenFilter.onReady(width,height);
3.onDrawFrame
// 1. 清理屏幕 设置屏幕颜色为 glClearColor中设置的颜色
GLES20.glClearColor(0,0,0,0);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 2. 把摄像头数据从 SurfaceTexture 中取出来
// 2.1 更新纹理,然后才能使用openGl从SurfaceTexture中获取数据
surfaceTexture.updateTexImage();
// 2.2 取得变换矩阵
// SurfaceTexture 在opengl中使用的是特殊的采样器“samplerExternalOES”,必须要通过变换矩阵才能获得 Simple2D的采样器
// mtx 代表一个4*4的矩阵数据,所以要用 float[] mtx = new float[16]来声明
surfaceTexture.getTransformMatrix(mtx);
// 3.把数据绘制到屏幕上显示
screenFilter.onDrawFrame(mTextures[0],mtx);
由此,我们的 render类就完成了,只要在 filter工具类中实现具体的获取数据和绘制的逻辑,就能把摄像头获取到的数据给显示出来了。
3.OpenGLUtils
在实现filter工具类之前,我们可以先编写一个处理着色器的工具类 OpenGLUtils,方便以后复用。
当前这个工具类中,只实现了一个 从raw文件中读取字符串的方法。
public static String readRawFileContent(Context context,int rawId){
InputStream inputStream = context.getResources().openRawResource(rawId);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
StringBuilder stringBuilder = new StringBuilder();
try {
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
} catch (Exception e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
4.编写 screen_vert.vert, screen_frag.frag
我们先回顾一下 顶点着色器 和 片元着色器 的概念。
我大概总结一下, 顶点着色器 决定了我们要画的形状,片元着色器负责填色。
opengl中 要通过 GLSL语言编写顶点着色器和片元着色器的代码,一般都写在raw文件中,也有写在代码中的,都可以。我们最终是要拿到的是代码的字符串内容。
我在代码里都写上了注释,有不懂的可以查查 GLSL语言的资料,最基本的写法按照下面这两个文件照着抄就行啦。
1.screen_vert.vert 顶点着色器代码
// 把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
//接收纹理坐标,接收采样器采样图片的坐标
attribute vec4 vCoord;
//变换矩阵, 需要将原本的vCoord(01,11,00,10) 与矩阵相乘 才能够得到 surfacetexure(特殊)的正确的采样坐标
uniform mat4 vMatrix;
//传给片元着色器 像素点
varying vec2 aCoord;
void main(){
//内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
gl_Position = vPosition;
aCoord = (vMatrix * vCoord).xy;
}
2.screen_frag.frag 片元着色器代码
//片元着色器 因为SurfaceTexture比较特殊,这里需要多加一句说明
#extension GL_OES_EGL_image_external : require
// 精度
precision mediump float;
// 采样点的坐标 来自顶点着色器
varying vec2 aCoord;
// 采样器 -- 需要从代码中传入
uniform samplerExternalOES vTexture;
void main() {
// 内置变量 接受像素值
// gl_FragColor = vec4(1,1,1,1);
// texture2D是内置函数,通过采样器采集到aCoord的颜色
gl_FragColor = texture2D(vTexture,aCoord);
}
5.ScreenFilter
开始了我们的重头戏。
代码中会用到的变量,这里先不做说明,联系下面的代码进行理解。
protected int mProgram;
protected final int vPosition,vCoord,vMatrix,vTexture;
protected int mWidth, mHeight;
protected FloatBuffer vPostionBuffer;
protected float[] POSITION = new float[]{-1f,-1f,
1f,-1f,
-1f,1f,
1f,1f};
protected FloatBuffer vCoordBuffer;
protected float[] TEXTURE = new float[]{0f,1f,
1f,1f,
0f,0f,
1f,0f};
这个类中,我们主要实现了三个方法。
1.初始化 ScreenFilter(Context context),在初始化的时候就可以通过顶点着色器和片元着色器代码创建好着色器程序,再从着色器程序中获取到我们需要赋值和处理的参数。这样我们后面就可以通过这些参数去改变着色器程序的效果。
//1.生成顶点着色器并编译顶点着色器代码
// 1.0 获取顶点着色器代码
String vertexSource = OpenGLUtils.readRawFileContent(context, R.raw.screen_vert);
// 1.1 生成顶点着色器id
int vShaderVextId = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
// 1.2 绑定代码到着色器中
GLES20.glShaderSource(vShaderVextId, vertexSource);
// 1.3 编译着色器代码
GLES20.glCompileShader(vShaderVextId);
// 1.4 主动获取成功 失败状态
int[] status = new int[1];
GLES20.glGetShaderiv(vShaderVextId, GLES20.GL_COMPILE_STATUS, status, 0);
if (status[0] != GLES20.GL_TRUE) {
// 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
throw new IllegalStateException(" 顶点着色器配置失败");
}
// 2.创建片元着色器并编译顶点着色器代码
// 2.0 获取片元着色器代码
String fragSource = OpenGLUtils.readRawFileContent(context, R.raw.screen_frag);
// 2.1 生成片元着色器id
int vShaderFragId = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
// 2.2 绑定代码到着色器中
GLES20.glShaderSource(vShaderFragId, fragSource);
// 2.3 编译着色器代码
GLES20.glCompileShader(vShaderFragId);
// 2.4 主动获取成功 失败状态
GLES20.glGetShaderiv(vShaderFragId, GLES20.GL_COMPILE_STATUS, status, 0);
if (status[0] != GLES20.GL_TRUE) {
// 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
throw new IllegalStateException(" 片元着色器配置失败");
}
// 3.创建着色器程序并链接着色器
mProgram = GLES20.glCreateProgram();
// 把着色器塞入 程序当中
GLES20.glAttachShader(mProgram, vShaderVextId);
GLES20.glAttachShader(mProgram, vShaderFragId);
// 链接着色器
GLES20.glLinkProgram(mProgram);
// 主动获取成功 失败状态
GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, status, 0);
if (status[0] != GLES20.GL_TRUE) {
// 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
throw new IllegalStateException(" 创建着色器程序失败");
}
// 4. 释放资源
GLES20.glDeleteShader(vShaderVextId);
GLES20.glDeleteShader(vShaderFragId);
// 以上 1,2,3,4是一个通用的写法,我们可以把这几个步骤写到 OpenGLUitils中,返回mProgram就行。
//5. 获得着色器中的参数变量的索引,后面通过这个索引给这个变量赋值,索引都是int类型的
vPosition = GLES20.glGetAttribLocation(mProgram,"vPosition");
vCoord = GLES20.glGetAttribLocation(mProgram,"vCoord");
vMatrix = GLES20.glGetUniformLocation(mProgram,"vMatrix");
vTexture = GLES20.glGetUniformLocation(mProgram,"vTexture");
//6. 创建顶点坐标和纹理坐标
// 顶点坐标
vPostionBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vPostionBuffer.clear();
vPostionBuffer.put(POSITION);
// 绘制的纹理坐标(android屏幕坐标)
// 关于 TEXTURE, 你会发现最终出来的相机画面会发生翻转和镜像,所以我们需要改变一下坐标,才能保证显示出来的画面是正确的。大家有兴趣可以试一试。
// 正确的坐标应该是 TEXTURE = new float[]{1f,0f,
// 1f,1f,
// 0f,0f,
// 0f,1f};
vCoordBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vCoordBuffer.clear();
vCoordBuffer.put(TEXTURE);
2.onReady(int width,int height)
this.mWidth = width;
this.mHeight = height;
3.具体绘制方法 onDrawFrame(int mTexture, float[] mtx)
// 1.设置窗口大小
GLES20.glViewport(0,0, mWidth,mHeight);
// 2.使用着色器程序
GLES20.glUseProgram(mProgram);
// 3.给着色器程序中传值
// 3.1 给顶点坐标数据传值
vPostionBuffer.position(0);
GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,0,vPostionBuffer);
// 激活
GLES20.glEnableVertexAttribArray(vPosition);
// 3.2 给纹理坐标数据传值
vCoordBuffer.position(0);
GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false,0,vCoordBuffer);
GLES20.glEnableVertexAttribArray(vCoord);
// 3.3 变化矩阵传值
GLES20.glUniformMatrix4fv(vMatrix,1,false,mtx,0);
// 3.4 给片元着色器中的 采样器绑定
// 激活图层
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
// 图像数据
GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES,mTexture);
// 传递参数
GLES20.glUniform1i(vTexture,0);
//参数传递完毕,通知 opengl开始画画
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
// 解绑
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
6.MyOpenGLView
自定义GLSurfaceView类
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
/**
* @author Lixingxing
*/
public class MyOpenGLView extends GLSurfaceView {
// 自定义的 Renderer
MyCameraRenderer cameraRenderer;
public MyOpenGLView(Context context) {
this(context,null);
}
public MyOpenGLView(Context context, AttributeSet attrs) {
super(context, attrs);
cameraRenderer = new MyCameraRenderer(this);
setEGLContextClientVersion(2);
setRenderer(cameraRenderer);
setRenderMode(RENDERMODE_WHEN_DIRTY);
}
// 屏幕锁屏或者按了home键或者页面关闭销毁的时候
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
cameraRenderer.surfaceDestroyed();
}
}
7. 在MainActivity中,使用 MyOpenGLView进行相机预览。注意,一定要申请权限,或者自己去设置中打开权限。不然会报错的哦。
import android.Manifest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.tbruyelle.rxpermissions2.RxPermissions;
import io.reactivex.functions.Consumer;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
checkPermissionRequest();
}
public void checkPermissionRequest() {
RxPermissions permissions = new RxPermissions(this);
permissions.setLogging(true);
permissions.request(Manifest.permission.CAMERA)
.subscribe(new Consumer() {
@Override
public void accept(Boolean aBoolean) throws Exception {
if(aBoolean){
setContentView(R.layout.activity_main);
}
}
});
}
4.小结:
这里只完成了最基本的用 GLSurfaceView实现相机预览功能。
代码很多,但是不难理解,其实就是调用openGL的api,很多代码都是可以复用的,作为程序员,ctrl+c ctrl+v都是基本操作了,熟悉了就很容易上手使用。下一章我们会对项目进行一下优化,方便我们以后实现更多功能的使用。