OpenGL ES 3.0学习实践

相机预览数据格式

android相机 输出的原始数据格式一般都是 NV21(实际上就是YUV420SP的格式) 或者 NV12(实际上就是YUV420P的格式) ,笔者的 小米MIX 2S 的默认输出格式就是 NV21 的,关于格式的问题,后续博客再详细说明。

前面的博客以及说明了如何通过 OpenGL ES 来渲染图像,一般 YUV格式 的数据是无法直接用 OpenGL ES 来渲染的,而在OpenGL中使用的绝大部分纹理ID都是RGBA的格式,在 OpenGL ES 3.0 的扩展 #extension GL_OES_EGL_image_external_essl3 定义了一个纹理的扩展类型,即 GL_TEXTURE_EXTERNAL_OES ,否则整个转换过程将会非常复杂。同时这种纹理目标对纹理的使用方式也会有一些限制,纹理绑定需要绑定到类型 GL_TEXTURE_EXTERNAL_OES 上,而不是类型GL_TEXTURE_2D上,对纹理设置参数也要使用 GL_TEXTURE_EXTERNAL_OES 类型,生成纹理与设置纹理参数的代码如下:

int[] tex = new int[1];
//创建一个纹理
GLES30.glGenTextures(1, tex, 0);
//绑定到外部纹理上
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0]);
//设置纹理过滤参数
GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);

在实际的渲染过程中绑定纹理的代码如下:

GLES30.glActiveTexture(GL_TEXTURE0);
GLES30.glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
GLES30.glUniform1i(uniformSamplers, 0);

在OpenGL ES的顶点着色器中,任何需要从纹理中采样的OpenGL ES的顶点着色器都需要声明其对此扩展的使用,使用指令如下:

//这行是OpenGL ES 2.0中的声明
#extension GL_OES_EGL_image_external : require
//这行是OpenGL ES 3.0中的声明
#extension GL_OES_EGL_image_external_essl3 : require

上面的过程就是使用这种扩展类型的纹理ID从创建到设置参数,再到真正的渲染整个过程,接下来再根据一个完整的示例看一下具体的旋转角度问题,因为在使用摄像头的时候很容易在预览的时候会出现倒立、镜像等问题。

开始项目实践

先不多说,直接实践看效果,基于之前的项目工程,新建CameraSurfaceRenderer.java文件:

* @anchor: andy * @date: 2018-11-09 * @description: 基于相机 public class CameraSurfaceRenderer implements GLSurfaceView.Renderer { private static final String TAG = "TextureRenderer"; private final FloatBuffer vertexBuffer, mTexVertexBuffer; private final ShortBuffer mVertexIndexBuffer; private int mProgram; private int textureId; * 顶点坐标 * (x,y,z) private float[] POSITION_VERTEX = new float[]{ 0f, 0f, 0f, //顶点坐标V0 1f, 1f, 0f, //顶点坐标V1 -1f, 1f, 0f, //顶点坐标V2 -1f, -1f, 0f, //顶点坐标V3 1f, -1f, 0f //顶点坐标V4 * 纹理坐标 * (s,t) private static final float[] TEX_VERTEX = { 0.5f, 0.5f, //纹理坐标V0 1f, 1f, //纹理坐标V1 0f, 1f, //纹理坐标V2 0f, 0.0f, //纹理坐标V3 1f, 0.0f //纹理坐标V4 private static final short[] VERTEX_INDEX = { 0, 1, 2, //V0,V1,V2 三个顶点组成一个三角形 0, 2, 3, //V0,V2,V3 三个顶点组成一个三角形 0, 3, 4, //V0,V3,V4 三个顶点组成一个三角形 0, 4, 1 //V0,V4,V1 三个顶点组成一个三角形 private float[] transformMatrix = new float[16]; * 渲染容器 private GLSurfaceView mGLSurfaceView; * 相机ID private int mCameraId; * 相机实例 private Camera mCamera; * Surface private SurfaceTexture mSurfaceTexture; * 矩阵索引 private int uTextureMatrixLocation; private int uTextureSamplerLocation; public CameraSurfaceRenderer(GLSurfaceView glSurfaceView) { this.mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT; this.mGLSurfaceView = glSurfaceView; mCamera = Camera.open(mCameraId); setCameraDisplayOrientation(mCameraId, mCamera); //分配内存空间,每个浮点型占4字节空间 vertexBuffer = ByteBuffer.allocateDirect(POSITION_VERTEX.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); //传入指定的坐标数据 vertexBuffer.put(POSITION_VERTEX); vertexBuffer.position(0); mTexVertexBuffer = ByteBuffer.allocateDirect( TEX_VERTEX.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(TEX_VERTEX); mTexVertexBuffer.position(0); mVertexIndexBuffer = ByteBuffer.allocateDirect(VERTEX_INDEX.length * 2) .order(ByteOrder.nativeOrder()) .asShortBuffer() .put(VERTEX_INDEX); mVertexIndexBuffer.position(0); private void setCameraDisplayOrientation(int cameraId, Camera camera) { Activity targetActivity = (Activity) mGLSurfaceView.getContext(); android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); android.hardware.Camera.getCameraInfo(cameraId, info); int rotation = targetActivity.getWindowManager().getDefaultDisplay() .getRotation(); int degrees = 0; switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; int result; if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { result = (info.orientation + degrees) % 360; result = (360 - result) % 360; // compensate the mirror } else { // back-facing result = (info.orientation - degrees + 360) % 360; camera.setDisplayOrientation(result); * 加载外部纹理 * @return public int loadTexture() { int[] tex = new int[1]; //创建一个纹理 GLES30.glGenTextures(1, tex, 0); //绑定到外部纹理上 GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0]); //设置纹理过滤参数 GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST); GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR); GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE); GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE); //解除纹理绑定 GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); return tex[0]; @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { //设置背景颜色 GLES30.glClearColor(0.5f, 0.5f, 0.5f, 0.5f); final int vertexShaderId = ShaderUtils.compileVertexShader(ResReadUtils.readResource(R.raw.vertex_camera_shader)); final int fragmentShaderId = ShaderUtils.compileFragmentShader(ResReadUtils.readResource(R.raw.fragment_camera_shader)); //链接程序片段 mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId); uTextureMatrixLocation = GLES30.glGetUniformLocation(mProgram, "uTextureMatrix"); //获取Shader中定义的变量在program中的位置 uTextureSamplerLocation = GLES30.glGetUniformLocation(mProgram, "yuvTexSampler"); //加载纹理 textureId = loadTexture(); //加载SurfaceTexture loadSurfaceTexture(textureId); @Override public void onSurfaceChanged(GL10 gl, int width, int height) { GLES30.glViewport(0, 0, width, height); @Override public void onDrawFrame(GL10 gl) { GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); //使用程序片段 GLES30. glUseProgram(mProgram); //更新纹理图像 mSurfaceTexture.updateTexImage(); mSurfaceTexture.getTransformMatrix(transformMatrix); //激活纹理单元0 GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //绑定外部纹理到纹理单元0 GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); //将此纹理单元床位片段着色器的uTextureSampler外部纹理采样器 GLES30.glUniform1i(uTextureSamplerLocation, 0); //将纹理矩阵传给片段着色器 GLES30.glUniformMatrix4fv(uTextureMatrixLocation, 1, false, transformMatrix, 0); GLES30.glEnableVertexAttribArray(0); GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); GLES30.glEnableVertexAttribArray(1); GLES30.glVertexAttribPointer(1, 2, GLES30.GL_FLOAT, false, 0, mTexVertexBuffer); // 绘制 GLES20.glDrawElements(GLES20.GL_TRIANGLES, VERTEX_INDEX.length, GLES20.GL_UNSIGNED_SHORT, mVertexIndexBuffer); public boolean loadSurfaceTexture(int textureId) { //根据纹理ID创建SurfaceTexture mSurfaceTexture = new SurfaceTexture(textureId); mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { mGLSurfaceView.requestRender(); }); //设置SurfaceTexture作为相机预览输出 try { mCamera.setPreviewTexture(mSurfaceTexture); } catch (IOException e) { e.printStackTrace(); return false; //开启相机预览 mCamera.startPreview(); return true;

顶点着色器:

#version 300 es
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec4 aTextureCoord;
//纹理矩阵
uniform mat4 uTextureMatrix;
out vec2 yuvTexCoords;
void main() { 
     gl_Position  = vPosition;
     gl_PointSize = 10.0;
     //只保留x和y分量
     yuvTexCoords = (uTextureMatrix * aTextureCoord).xy;

片段着色器:

#version 300 es
//OpenGL ES3.0外部纹理扩展
#extension GL_OES_EGL_image_external_essl3 : require
precision mediump float;
uniform samplerExternalOES yuvTexSampler;
in vec2 yuvTexCoords;
out vec4 vFragColor;
void main() {
     vFragColor = texture(yuvTexSampler,yuvTexCoords);

注意: 外部纹理扩展在OpenGL ES 2.0OpenGL ES 3.0中不太一样。

//OpenGL ES2.0外部纹理扩展
#extension GL_OES_EGL_image_external : require

在启动的Activity中处理初始化的部分:

private void setupView() {
     //实例化一个GLSurfaceView
     mGLSurfaceView = new GLSurfaceView(this);
     mGLSurfaceView.setEGLContextClientVersion(3);
     mGLSurfaceView.setRenderer(new CameraSurfaceRenderer(mGLSurfaceView));
     setContentView(mGLSurfaceView);

来个剪刀手看看效果:

简单的滤镜处理

通过纹理将相机采集的数据渲染到GLSurfaceView的过程中,我们也可以添加各种滤镜。

给相机添加黑白滤镜,修改片段着色器

#version 300 es
//OpenGL ES3.0外部纹理扩展
#extension GL_OES_EGL_image_external_essl3 : require
precision mediump float;
uniform samplerExternalOES yuvTexSampler;
in vec2 yuvTexCoords;
out vec4 vFragColor;
void main() {
     vec4 vCameraColor = texture(yuvTexSampler,yuvTexCoords);
     float fGrayColor = (0.3*vCameraColor.r + 0.59*vCameraColor.g + 0.11*vCameraColor.b);
     vFragColor = vec4(fGrayColor, fGrayColor, fGrayColor, 1.0);

可以参照下面的几个转换公式:

标清电视使用的标准BT.601

\begin{bmatrix} Y^{'} \\ U \\ V \\ \end{bmatrix} = \begin{bmatrix} 0.299 & 0.587 & 0.114\\ -0.14713 & -0.28886 & 0.436 \\ 0.615 & -0.51499 & -0.10001 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \\ \end{bmatrix} YUV= 0.2990.147130.6150.5870.288860.514990.1140.4360.10001RGB

\begin{bmatrix} R \\ G \\ B \\ \end{bmatrix} = \begin{bmatrix} 1 & 0 & 1.13983\\ 1 & -0.39465 & -0.58060 \\ 1 & 2.03211 & 0 \end{bmatrix} \begin{bmatrix} Y^{'} \\ U \\ V \\ \end{bmatrix} RGB=11100.394652.032111.139830.580600YUV

标清电视使用的标准BT.709

\begin{bmatrix} Y^{'} \\ U \\ V \\ \end{bmatrix} = \begin{bmatrix} 0.2126 & 0.7152 & 0.0722\\ -0.09991 & -0.33609 & 0.436 \\ 0.615 & -0.55861 & -0.05639 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \\ \end{bmatrix}
Y UV=0.21260.099910.6150.71520.336090.558610.07220.4360.05639RGB

\begin{bmatrix} R \\ G \\ B \\ \end{bmatrix} = \begin{bmatrix} 1 & 0 & 1.28033\\ 1 & -0.21482 & -0.38059 \\ 1 & 2012798 & 0 \end{bmatrix} \begin{bmatrix} Y^{'} \\ U \\ V \\ \end{bmatrix} RGB=11100.2148220127981.280330.380590YUV

上述的例子完整的实现了相机预览数据通过OpenGL ES 3.0实时渲染到GLSurfaceView上。但是如果你稍不注意,很可能会出现手机摄像头预览的时候会出现倒立、镜像等问题,下面来看看这个过程。

前置摄像头从采集到最终显示的过程:

后置摄像头从采集到最终显示的过程:

OpenGL坐标:

OpenGL二维纹理坐标:

不做任何旋转的纹理坐标:

private static final float[] TEX_VERTEX = {
        0.0f, 0.0f,     //图像左下角
        1.0f, 0.0f,     //图像右下角
        0.0f, 1.0f,   	//图像左上角
        1.0f, 1.0f    	//图像右上角

顺时针旋转90度的纹理坐标:(可以想象一下将上述的OpenGL二维纹理坐标顺时针旋转90度)

private static final float[] TEX_VERTEX = {
 		1.0f, 0.0f,     //图像右下角
 		1.0f, 1.0f,    	//图像右上角
        0.0f, 0.0f,     //图像左下角
        0.0f, 1.0f   	//图像左上角

顺时针旋转180度的纹理坐标:

private static final float[] TEX_VERTEX = {
 		1.0f, 0.0f,     //图像右下角
 		0.0f, 1.0f,   	//图像左上角
 		1.0f, 1.0f,    	//图像右上角
        0.0f, 0.0f      //图像左下角

顺时针旋转270度的纹理坐标:

private static final float[] TEX_VERTEX = {
		0.0f, 1.0f,   	//图像左上角
		0.0f, 0.0f,     //图像左下角
		1.0f, 1.0f,    	//图像右上角
 		1.0f, 0.0f     //图像右下角

我们再来看看之前说的计算机中的图像坐标系:

所以我们实际在处理相机的预览图像时要对每一个纹理坐标做一个VFlip的变换(即把每一个顶点的y值由0变为1或者由1变为0),这样就可以得到一个正确的图像旋转了。而前置摄像头还存在镜像的问题,因此需要对每一个纹理坐标做一个HFlip的变换(即把每一个顶点的x值由0变为1或者由1变为0),从而让图片在预览界面中看起来就像在镜子中的一样。

项目地址:
https://github.com/byhook/opengles4android

参考:
https://blog.csdn.net/lb377463323/article/details/77071054#t0
《音视频开发进阶指南》

android平台下OpenGL ES 3.0对相机Camera预览实时处理摄像头的预览配置好摄像头之后,剩下的事情就是配置摄像头采集每一帧图像的回调,并且获取到图像之后将图像渲染到屏幕上。本书的第4章已经讲解过了如何通过OpenGL ES来渲染图像,这里先来回顾一下:首先把图像解码为RGBA格式;然后将RGBA格式的字节数组上传到一个纹理上;最终将该纹理渲染到屏幕上。所以这里的渲染到屏幕上也... 在前面使用OpenGLEs的过程中由于使用的是OpenGlEs3.0的版本所以会带来不少因为版本兼容问题的坑(开始的时候没有发现,因为一直使用的是Android8.1.0的系统的手机。后来测试兼容性的时候发现了坑,我这里最低版本的手机是装有4.4.2系统的)。 问题产生的原因是因为Android系统随着版本的不同里面自带的OpenGL ES版本也会随之不同,而在我们编写GLSL文件的时候如果... 最简单的办法,就是转换为RGB。这个有现在的代码,吾亦提供了一个: https://blog.csdn.net/quantum7/article/details/105720150   性能较差。有...
一、问题描述 在将一份OpenGL ES的片段着色器从 GLSL 2.0 改成 GLSL 3.0语法的时候,编译完之后运行,报错了,如下所示: 2021-11-28 17:13:18.898 14115-14199/com.oyp.openglesdemo I/AdrenoGLES-0: ERROR: 0:32: 'texture2D' : type is for Vulkan api only ERROR: 0:32: 'texture2D' : cannot construct this
一、相机简介   在Android OpenGL基础(三、绘制Bitmap纹理)一文中,我们简单介绍了如何绘制如何把一张图片贴到四边形上。本文介绍如何用GLSurfaceView来实现预览相机。与单张图片纹理不同的地方在于,相机是一个内容不断变化的纹理。   首先,先简单介绍相机的几个常用方法: 1.1 声明相机权限   如果APP需要使用相机,则需要在manifest.xml中声明: <uses-permission android:name="android.permission.CAMERA"
OpenGL ES 3.0 上的一个三角形例子,网上可以下载到android skd 版(java)和 android ndk (c&c++版) 为了了解一下JNI,于是写了如下小程序。 这个例子是使用jni, java中调用c中的代码完成三角形的渲染, 其中shader代码保存在assets目录下,如下目录图:
Android OpenGL+Camera2渲染(1) —— OpenGL简单介绍 Android OpenGL+Camera2渲染(2) —— OpenGL实现Camera2图像预览 Android OpenGL+Camera2渲染(3) —— 大眼,贴纸功能实现 Android OpenGL+Camera2渲染(4) —— 美颜功能实现 Android OpenGL+Camera2渲染...
GLSurfaceViewAndroid平台上用于渲染OpenGL ES图形的视图。要从GLSurfaceView中获取图像,你可以使用Android的截屏功能来实现。 你可以使用以下代码来截取GLSurfaceView的图像: // 获取GLSurfaceView实例 GLSurfaceView glSurfaceView = ...; // 获取GLSurfaceView的Bitmap Bitmap bitmap = glSurfaceView.getDrawingCache(); // 保存Bitmap到文件 FileOutputStream out = new FileOutputStream("screenshot.png"); bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); out.close(); 请注意,在使用getDrawingCache()方法之前,你需要先调用GLSurfaceView的setDrawingCacheEnabled(true)方法。 需要注意的是,GLSurfaceView的截图可能并不总是准确的,因为它可能在渲染时使用了缓存,或者因为屏幕的刷新率导致图像存在残影。如果你需要更精确的截图,你可以考虑使用OpenGL ES的帧缓冲区(Framebuffer Object)或者其他技术来实现。

相机从采集到显示的过程