Android AR开发实践之六:OpenGLES+ARcore绘制摄像头数据到SurfaceView

Android AR开发实践之六:OpenGLES+ARcore绘制摄像头数据到SurfaceView

1、开发环境搭建

首先创建一个Empty Activity的新工程。

  • 在App的build.gradle文件中加入ARCore的依赖
    implementation 'com.google.ar:core:1.22.0'
  • 在manifest文件中添加权限声明
 <!-- Camera permission  -->
    <uses-permission android:name="android.permission.CAMERA" />

    <!-- Sceneform requires OpenGLES 3.0 or later. -->
    <uses-feature android:glEsVersion="0x00030000" android:required="true" />

    <!-- Indicates that this app requires Google Play Services for AR ("AR Required") and results in the app only being visible in the Google Play Store on devices that support ARCore. For an "AR Optional" app, remove this tag. -->
    <uses-feature android:name="android.hardware.camera.ar" android:required="true"/>

以及

<!-- Indicates that this app requires Google Play Services for AR ("AR Required") and causes
         the Google Play Store to download and intall Google Play Services for AR along with
         the app. For an "AR Optional" app, specify "optional" instead of "required". -->
        <meta-data android:name="com.google.ar.core" android:value="required" />

2、添加GLSurfaceView到工程中

2.1、 在activity_main.xml中添加GLSurfaceView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.opengl.GLSurfaceView
        android:id="@+id/glSurfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2.2、 初始化GLSurfaceView

GLSurfaceView继承自SurfaceView,是对SurfaceView再做了一次封装,可以看作是SurfaceView的一种典型使用模式,方便我们在实际开发中方便的使用OpenGL。

它主要是在SurfaceView的基础上它加入了EGL的管理,并自带了一个GLThread绘制线程(EGLContext创建GL环境所在线程即为GL线程),绘制的工作直接通过OpenGL在绘制线程进行,不会阻塞主线程,绘制的结果输出到SurfaceView所提供的Surface上,这使得GLSurfaceView也拥有了OpenGLES所提供的图形处理能力,通过它定义的Render接口,使更改具体的Render的行为非常灵活性,只需要将实现了渲染函数的Renderer的实现类设置给GLSurfaceView即可。

 		// 初始化GLSurfaceView,并跟TapHelper绑定
        surfaceView = findViewById(R.id.glSurfaceView);

        // 配置GLSurfaceView基本属性, 并设置renderer.
        
       //控制当GLSurfaceView暂停和恢复的时候是否保存EGLContext。
       //如果设置为true,然后EGLContext在GLSurfaceView被暂停的时候被保存
       //如果设置为false,EGL Context在GLSurfaceView暂停的时候会被释放,然后在GLSurfaceView恢复的时候重建     
surfaceView.setPreserveEGLContextOnPause(true);

        //通知EGLContext客户端版本选择的默认的EGLContextFactory和默认的EGLConfigChooser。如果这个方法被调动,它必须在setRender(Render)被调用之前。这个方法仅仅影响默认EGLContextFactory和默认EGLConfigChooser的行为。
        surfaceView.setEGLContextClientVersion(3);

        //一个给定的Android设备可能支持多个EGLConfig渲染配置。可用的配置可能在有多少个数据通道和分配给每个数据通道的比特数上不同。默认情况下,GLSurfaceView选择的EGLConfig有RGB_888像素格式,至少有16位深度缓冲和没有模板。安装一个ConfigChooser,它将至少具有指定的depthSize和stencilSize的配置,并精确指定指定的redSize、greenSize、blueSize和alphaSize。
        surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
        
        //设置GLSurfaceView相关联的渲染器,还将启动将调用渲染器的线程,从而开始渲染。这个方法在GLSurfaceView生命周期内应该且仅仅能被调用一次
surfaceView.setRenderer(this);

        //设置渲染模式,当渲染模式是RENDERMODE_CONTINUOUSLY,渲染器被反复调用以重绘屏幕。当设置模式是RENDERMODE_WHEN_DIRTY的时候,渲染器只有在surface被创建的时候,或者当requestRender()的时候被调用,默认为READERMODE_CONTINUOUSLY。使用RENDERMODE_WHEN_DIRTY,当视图不需要更新时,允许GPU和CPU空闲。可以提升电池生命和整体的系统性能。
         surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);

2.3、 实现Renderer接口

GLSurfaceView.Renderer接口提供了SurfaceView的渲染,该接口有三个方法:

  • onSurfaceCreated

它是在Surface创建的时候调用,可以在这里进行一些初始化操作,它携带两个参数GL10 和 EGLConfig。

GL10 继承自GL的一个接口,封装了程序语言为OpenGL绑定的核心功能,我们可以把它理解为一个“画笔”,操作它提供的的方法来在GLSurfaceView上这块“画布”绘制

EGLConfig 是一个抽象类,但是没有任何实现。

e.g:
在onSurfaceCreated里调用

gl.glClearColor(0f, 1f, 0f, 0f)设置清屏颜色为绿色,所谓清屏颜色就是当开始绘制时,屏幕所显示的颜色。

glClearColor(float red, float green, float blue, float alpha)四个参数对应红、绿、蓝、透明度。这里的取值范围是0到1f。不是RGB中0~255。

  • onSurfaceChanged

它是在Surface大小改变的时候调用,这个方法只会执行一次,Surface创建时初始大小为0,所以需要指定窗口大小,调用glViewport(int x, int y, int width, int height)方法传入四个参数,x,y坐标和宽高。
这里要注意一点,这个窗口坐标原点是位于屏幕左下角,与android中的屏幕坐标系不一样

  • onDrawFrame

它是在Surface上绘制的时候调用。

1.通过setRenderMode设置渲染模式,有两种供选择

GLSurfaceView.RENDERMODE_CONTINUOUSLY 不间断的绘制,默认渲染模式是这种

GLSurfaceView.RENDERMODE_WHEN_DIRTY 在屏幕变脏时绘制,也就是当调用GLSurfaceView的requestRender ()方法后才会执行一次(第一次运行的时候会自动绘制一次)

2.调用glClear(GL10.GL_COLOR_BUFFER_BIT)方法清除屏幕颜色,执行这个方法之后,屏幕就会渲染之前通过glClearColor设置的清屏颜色.如下图所示

如果设置渲染模式为GLSurfaceView.RENDERMODE_CONTINUOUSLY那么onDrawFrame方法会一直执行。

下面的代码演示了如何创建一个surfaceView的对象,并在初始化时绘制背景颜色

public class MainActivity extends AppCompatActivity implements GLSurfaceView.Renderer{

    // Surface View
    private GLSurfaceView surfaceView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 初始化GLSurfaceView,并跟TapHelper绑定
        surfaceView = findViewById(R.id.glSurfaceView);

        // 配置GLSurfaceView基本属性, 并设置renderer.
        surfaceView.setPreserveEGLContextOnPause(true);
        surfaceView.setEGLContextClientVersion(3);
        surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
        surfaceView.setRenderer(this);
        surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }
   
    /********************* implements GLSurfaceView.Renderer  ******************/
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
     	// 设置清屏颜色
        GLES30.glClearColor(0f, 2f, 3f, 1.0f);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // 设置窗口大小
        GLES30.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
		//当绘制一帧时,这个方法会被GLSurfaceView调用
        //清空屏幕上颜色,用之前glClearColor()调用定义色颜色填充整个屏幕,不能加载前面帧的任何像素 
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);
    }
}

3、绘制Camera数据到GLSurfaceView

了解了如何使用GLSurfaceView之后,接下来看一下如何使用ARCore SDK将Camera视图绘制到SurfaceView上。

首先回顾一下前面提到的ARCore的类:

  • Session

这个类是ARCore中最重要的一个类,先看一下官方对它的一些介绍

Session管理者AR系统的状态和生命周期. Session是ARCore的入口,用户通过它来创建、配置一个AR场景,或者是启动、停止AR。

Session还有一个最重要的功能就是通过Session可以获取Camera的当前帧(Frame), 只要拿到Frame我们就可以访问Camera中的数据

  • Frame

刚才说了通过Session可以拿到当前帧(Frame), 而这个Frame内部就是封装了Camera的状态已经数据。它有个很重要的方法 getCamera() 返回的就是AR Session所请求到的设备Camera对象。

有了Camera之后,就可以通过这个Camera的 getProjectionMatrix 和 getViewMatrix 方法分别初始化ProjectMatrix和ViewMatrix了。而这两个Matrix都是OpenGL绘制时所需要的。

然后来了解一下ARCore使用的流程:

第一步:在Activity的 onCreate 方法中对Session进行创建工作,在 onResume 方法中调用 Session.resume 方法。

第二步:在 GLSurfaceView.Renderer 的回调方法 onDrawFrame 中通过调用 Session.update 方法获取 Frame 对象, 然后调用OpenGL的相关方法将Frame绘制到GLSurfaceView上。

在这里插入图片描述

3.1、创建Session

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ......
        createSession();
    }

    @Override
    protected void onResume() {
        super.onResume();

        try {
            session.resume();
        } catch (CameraNotAvailableException e) {
            // 有些情况下,手机Camera正在被其它的App所使用。这种情况下可能会报Camera Not Available异常
            session = null;
            return;
        }

        surfaceView.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
        if (session != null) {
            // 注意:顺序不能改变!必须先暂停GLSurfaceView, 否则GLSurfaceView会继续调用Session的update方法。
            // 但是Session已经pause状态,所以会报SessionPausedException异常
            surfaceView.onPause();
            session.pause();
        }
    }

    private void createSession(){
        if (session == null) {
            try {
                switch (ArCoreApk.getInstance().requestInstall(this, !installRequested)) {
                    case INSTALL_REQUESTED:
                        installRequested = true;
                        return;
                    case INSTALLED:
                        break;
                }

                // ARCore需要申请并处理Camera的操作,因此必须动态申请Camera相关的权限
                if (!CameraPermissionHelper.hasCameraPermission(this)) {
                    CameraPermissionHelper.requestCameraPermission(this);
                    return;
                }

                // 创建session对象,Session是ARCore中真正用来与设备Camera进行打交道的类
                // 内部实现中进行了Camera的相关读取与计算,之后通过它可以获取Camera的当前帧Frame
                session = new Session(/* context= */ this);
            } catch (Exception e) {
                Log.e(TAG, "Failed to create AR session: " + e.getMessage());
                return;
            }
        }
    }

3.2、获取Frame对象并绘制

 @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //GLES30.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);

        try {
            // 初始化用来画背景以及Virtual Object的OpenGL设置
            // 主要包括各种OpenGL需要使用的textureId, Texture Coordinates, Shader, Program等
            backgroundRenderer.createOnGlThread(this);
        } catch (IOException e) {
            Log.e(TAG, "Failed to read an asset file", e);
        }
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0, width, height);

        // 当SurfaceView发生change时,需要重新设置Session的rotation, width, height
        int displayRotation = getSystemService(WindowManager.class).getDefaultDisplay().getRotation();
        session.setDisplayGeometry(displayRotation, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);

        if (session == null) {
            return;
        }

        try {
            // 将在'createOnGlThread'方法中已经初始化好的Texture Handle(句柄)传给AR Session
            // 如果没有设置此句柄,则会显示黑屏。
            session.setCameraTextureName(backgroundRenderer.getTextureId());

            // 通过AR Session获取当前手机摄像头(Camera)的当前帧(Frame)。
            Frame frame = session.update();

            // 将当前帧Frame当做背景来draw到SurfaceView上,因此我们能在手机屏幕上看到摄像头中的实时内容
            backgroundRenderer.draw(frame);
        } catch (Exception e) {

        }
    }

3.3、最后贴上OpenGL具体绘制源码(源自GoogleDemo)

BackgroundRenderer.java


public class BackgroundRenderer {
  private static final String TAG = BackgroundRenderer.class.getSimpleName();

  // Shader names.
  private static final String VERTEX_SHADER_NAME = "shaders/screenquad.vert";
  private static final String FRAGMENT_SHADER_NAME = "shaders/screenquad.frag";

  private static final int COORDS_PER_VERTEX = 3;
  private static final int TEXCOORDS_PER_VERTEX = 2;
  private static final int FLOAT_SIZE = 4;

  private FloatBuffer quadVertices;
  private FloatBuffer quadTexCoord;
  private FloatBuffer quadTexCoordTransformed;

  private int quadProgram;

  private int quadPositionParam;
  private int quadTexCoordParam;
  private int textureId = -1;

  public BackgroundRenderer() {}

  public int getTextureId() {
    return textureId;
  }

  /**
   * Allocates and initializes OpenGL resources needed by the background renderer. Must be called on
   * the OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10,
   * EGLConfig)}.
   *
   * @param context Needed to access shader source.
   */
  public void createOnGlThread(Context context) throws IOException {
    // Generate the background texture.
    int[] textures = new int[1];
    GLES30.glGenTextures(1, textures, 0);
    textureId = textures[0];
    int textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
    GLES30.glBindTexture(textureTarget, textureId);
    GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
    GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
    GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
    GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_NEAREST);

    int numVertices = 4;
    if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) {
      throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer.");
    }

    ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE);
    bbVertices.order(ByteOrder.nativeOrder());
    quadVertices = bbVertices.asFloatBuffer();
    quadVertices.put(QUAD_COORDS);
    quadVertices.position(0);

    ByteBuffer bbTexCoords =
        ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
    bbTexCoords.order(ByteOrder.nativeOrder());
    quadTexCoord = bbTexCoords.asFloatBuffer();
    quadTexCoord.put(QUAD_TEXCOORDS);
    quadTexCoord.position(0);

    ByteBuffer bbTexCoordsTransformed =
        ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
    bbTexCoordsTransformed.order(ByteOrder.nativeOrder());
    quadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer();

    int vertexShader =
        ShaderUtil.loadGLShader(TAG, context, GLES30.GL_VERTEX_SHADER, VERTEX_SHADER_NAME);
    int fragmentShader =
        ShaderUtil.loadGLShader(TAG, context, GLES30.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_NAME);

    quadProgram = GLES30.glCreateProgram();
    GLES30.glAttachShader(quadProgram, vertexShader);
    GLES30.glAttachShader(quadProgram, fragmentShader);
    GLES30.glLinkProgram(quadProgram);
    GLES30.glUseProgram(quadProgram);

    ShaderUtil.checkGLError(TAG, "Program creation");

    quadPositionParam = GLES30.glGetAttribLocation(quadProgram, "a_Position");
    quadTexCoordParam = GLES30.glGetAttribLocation(quadProgram, "a_TexCoord");

    ShaderUtil.checkGLError(TAG, "Program parameters");
  }

  /**
   * Draws the AR background image. The image will be drawn such that virtual content rendered with
   * the matrices provided by {@link com.google.ar.core.Camera#getViewMatrix(float[], int)} and
   * {@link com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)} will
   * accurately follow static physical objects. This must be called <b>before</b> drawing virtual
   * content.
   *
   * @param frame The last {@code Frame} returned by {@link Session#update()}.
   */
  public void draw(Frame frame) {
    // If display rotation changed (also includes view size change), we need to re-query the uv
    // coordinates for the screen rect, as they may have changed as well.
    if (frame.hasDisplayGeometryChanged()) {
      frame.transformDisplayUvCoords(quadTexCoord, quadTexCoordTransformed);
    }

    // No need to test or write depth, the screen quad has arbitrary depth, and is expected
    // to be drawn first.
    GLES30.glDisable(GLES30.GL_DEPTH_TEST);
    GLES30.glDepthMask(false);

    GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);

    GLES30.glUseProgram(quadProgram);

    // Set the vertex positions.
    GLES30.glVertexAttribPointer(
        quadPositionParam, COORDS_PER_VERTEX, GLES30.GL_FLOAT, false, 0, quadVertices);

    // Set the texture coordinates.
    GLES30.glVertexAttribPointer(
        quadTexCoordParam,
        TEXCOORDS_PER_VERTEX,
        GLES30.GL_FLOAT,
        false,
        0,
        quadTexCoordTransformed);

    // Enable vertex arrays
    GLES30.glEnableVertexAttribArray(quadPositionParam);
    GLES30.glEnableVertexAttribArray(quadTexCoordParam);

    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);

    // Disable vertex arrays
    GLES30.glDisableVertexAttribArray(quadPositionParam);
    GLES30.glDisableVertexAttribArray(quadTexCoordParam);

    // Restore the depth state for further drawing.
    GLES30.glDepthMask(true);
    GLES30.glEnable(GLES30.GL_DEPTH_TEST);

    ShaderUtil.checkGLError(TAG, "Draw");
  }

  private static final float[] QUAD_COORDS =
      new float[] {
        -1.0f, -1.0f, 0.0f, -1.0f, +1.0f, 0.0f, +1.0f, -1.0f, 0.0f, +1.0f, +1.0f, 0.0f,
      };

  private static final float[] QUAD_TEXCOORDS =
      new float[] {
        0.0f, 1.0f,
        0.0f, 0.0f,
        1.0f, 1.0f,
        1.0f, 0.0f,
      };
}

ShaderUtil.java

/** Shader helper functions. */
public class ShaderUtil {
  /**
   * Converts a raw text file, saved as a resource, into an OpenGL ES shader.
   *
   * @param type The type of shader we will be creating.
   * @param filename The filename of the asset file about to be turned into a shader.
   * @return The shader object handler.
   */
  public static int loadGLShader(String tag, Context context, int type, String filename)
      throws IOException {
    String code = readRawTextFileFromAssets(context, filename);
    int shader = GLES30.glCreateShader(type);
    GLES30.glShaderSource(shader, code);
    GLES30.glCompileShader(shader);

    // Get the compilation status.
    final int[] compileStatus = new int[1];
    GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compileStatus, 0);

    // If the compilation failed, delete the shader.
    if (compileStatus[0] == 0) {
      Log.e(tag, "Error compiling shader: " + GLES30.glGetShaderInfoLog(shader));
      GLES30.glDeleteShader(shader);
      shader = 0;
    }

    if (shader == 0) {
      throw new RuntimeException("Error creating shader.");
    }

    return shader;
  }

  /**
   * Checks if we've had an error inside of OpenGL ES, and if so what that error is.
   *
   * @param label Label to report in case of error.
   * @throws RuntimeException If an OpenGL error is detected.
   */
  public static void checkGLError(String tag, String label) {
    int lastError = GLES30.GL_NO_ERROR;
    // Drain the queue of all errors.
    int error;
    while ((error = GLES30.glGetError()) != GLES30.GL_NO_ERROR) {
      Log.e(tag, label + ": glError " + error);
      lastError = error;
    }
    if (lastError != GLES30.GL_NO_ERROR) {
      throw new RuntimeException(label + ": glError " + lastError);
    }
  }

  /**
   * Converts a raw text file into a string.
   *
   * @param filename The filename of the asset file about to be turned into a shader.
   * @return The context of the text file, or null in case of error.
   */
  private static String readRawTextFileFromAssets(Context context, String filename)
      throws IOException {
    try (InputStream inputStream = context.getAssets().open(filename);
         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
      StringBuilder sb = new StringBuilder();
      String line;
      while ((line = reader.readLine()) != null) {
        sb.append(line).append("\n");
      }
      return sb.toString();
    }
  }
}

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值