Android Camera开实践(2)opengl使用

2,250 阅读9分钟

欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵

本篇文章不讲opengl技术,重点放在Android端如何接入opengl。代码参考Android官方文档实现。完整代码已上传到github

github.com/summer-go/A…

如果你是opengl零基础,可以先了解opengl,或者读完本篇文章后,抽时间补充。

实现效果

基于touch事件,转动三角形,通过实现这个小demo,来完整了解Android端opengl的基础开发

2798d897-5706-4e1a-b205-03392f6a77be.gif

构建OpenGL ES环境

简化开发流程,我们基于GLSurfaceView来开发,并使用Android/Java层的OpenGL ES API,下一篇会讲述在native层开发opengl。OpenGL版本选择2.0,你也可以选择3.0或更高版本,不影响基础开发的学习。

AndroidManifest.xml增加声明

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

如果是GL3.0则声明为

<uses-feature android:glEsVersion="0x00030000" android:required="true" />

增加GLSurfaceView

GLSurfaceView继承自SurfaceView,实现内部接口GLSurfaceView.Renderer,以控制实际的渲染逻辑,而GLSurfaceView则提供GL环境,如此大大简化了opengl的环境配置。

一个Activity来组织所有逻辑

//Activity里添加GLSurfaceView
public class OpenGLActivity extends AppCompatActivity {

    private MyGLSurfaceView glView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        glView = new MyGLSurfaceView(this);
        setContentView(glView);
    }
}   

实现MyGLSurfaceView

import android.content.Context;
import android.opengl.GLSurfaceView;

class MyGLSurfaceView extends GLSurfaceView {
    private final MyGLRenderer renderer;
    public MyGLSurfaceView(Context context){
        super(context);
        // Create an OpenGL ES 2.0 context
        // EGL是链接opengl和底层硬件显示的桥梁
        setEGLContextClientVersion(2);

        renderer = new MyGLRenderer();
        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(renderer);

        // 设置渲染模式,两种场景各有优点
        // WHEN_DIRTH 有新的数据才出发绘制,对性能友好
        // CONTINUOUSLY 按一定频率持续刷新,不断的触发绘制
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }
}

设置Renderer,方便实现demo逻辑,写成MyGlSurfaceView的内部静态类

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;

public class MyGLRenderer implements GLSurfaceView.Renderer {

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // 设置opengl的清屏颜色
        // (0.0f, 0.0f, 0.0f, 1.0f)表示RGBA4通道,这里指黑色
        // (1.0f, 0.0f, 0.0f, 1.0f)则表示红色
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    }

    public void onDrawFrame(GL10 unused) {
        // 调用gl的清屏操作,每次渲染之前,最好清除屏幕上一次的缓存颜色
        // 清屏颜色onSurfaceCreated中设置为黑色,可以设置成任意颜色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }

    public void onSurfaceChanged(GL10 unused, int width, int height) {
        // 设置窗口属性(0, 0)表示原点坐标
        // width height表示显示的窗口大小
        GLES20.glViewport(0, 0, width, height);
    }
}
    

EGL简介

EGL,全称:embedded Graphic Interface,是 OpenGL ES 和底层 Native 平台 视窗系统之间的接口。EGL和OpenGL一样,也是一个通用标准,各个平台、硬件的实现不同。iphone上不支持EGL,用的是EAGL。

OpenGL ES 是负责 GPU 工作的,目的是通过 GPU 计算,得到一张图片,这张图片在内存中其实就是一块 buffer,存储有每个点的颜色信息等。而这张图片最终是要显示到屏幕上,所以还需要具体的窗口系统来操作,OpenGL ES 并没有相关的函数。所以,OpenGL ES 有一个好搭档 EGL。

(www.cnblogs.com/yongdaimi/p… "OpenGL ES 与 EGL、GLSL的关系")

二、绘制三角形

先定义一个三角形

如果你的opengl是零基础,可能会有点吃力,建议后面补上相关知识。

public class Triangle {

    private FloatBuffer vertexBuffer;

    // 定义每个顶点三个坐标,即x、y、z分量
    static final int COORDS_PER_VERTEX = 3;
    static float triangleCoords[] = {  
              // in counterclockwise order:
              // 三角形的顶点按逆时针顺序定义,用于检测前后,此处可以先不用管
             0.0f,  0.622008459f, 0.0f, // top
            -0.5f, -0.311004243f, 0.0f, // bottom left
             0.5f, -0.311004243f, 0.0f  // bottom right
    };

    // 三角形顶点颜色
    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    public Triangle() {
        // 初始化顶点数据的buffer
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // 每个坐标用4字节存储
                triangleCoords.length * 4);
        // 采用设备硬件的本地字节序
        bb.order(ByteOrder.nativeOrder());
        // create a floating point buffer from the ByteBuffer
        vertexBuffer = bb.asFloatBuffer();
        // add the coordinates to the FloatBuffer
        vertexBuffer.put(triangleCoords);
        // set the buffer to read the first coordinate
        vertexBuffer.position(0);
    }
}

注意:opengl es底层调用的是C/C++,数据结构的定义不同,不能直接用java层的api来定义,或涉及到数据转换、效率较低。

FloatBuffer说明

(stackoverflow.com/questions/1… "why FloatBuffer")

四边形的定义读者可参考官方文档 (developer.android.google.cn/training/gr… "定义形状")

原理相同,区别是四边形要定义两个三角形。

绘制三角形

三角形顶点属性一般不会改变,onSurfaceCreated中创建一次即可

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...
    private Triangle mTriangle;

    // 定义顶点着色器,暂时点不做任何处理
    private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
        "void main() {" +
        "  gl_Position = vPosition;" +
        "}";
    
    // 片段着色器,暂时啥也没干
    private final String fragmentShaderCode =
        "precision mediump float;" +
        "uniform vec4 vColor;" +
        "void main() {" +
        "  gl_FragColor = vColor;" +
        "}";

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        ...
        // initialize a triangle
        mTriangle = new Triangle();
    }
    ...
}

加载glsl shader的方法,定义在Renderer中

// 加载完,返回一个句柄,后面要用到
public static int loadShader(int type, String shaderCode) {
    // create a vertex shader type(GLES20.GL_VERTEX_SHADER)
    // or a fragment shader tye(GLES20.GL_FRAGMENT_SHADER)
    int shader = GLES20.glCreateShader(type);

    // add the source code to the shader and compile it
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);

    return shader;
}

回到Triangle中,实现shader的加载、编译和链接,增加draw方法

public class Triangle() {
    ...
    private final int mProgram;
    public Triangle() {
        ...
        // 加载顶点着色器
        int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                                        vertexShaderCode);
        // 加载片元着色器   
        int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
                                        fragmentShaderCode);
        // 创建一个空的OpenGL ES Program
        mProgram = GLES20.glCreateProgram();
        // 添加vertexShder
        GLES20.glAttachShader(mProgram, vertexShader);
        // 添加fragmentShader
        GLES20.glAttachShader(mProgram, fragmentShader);
        // 创建一个可执行的program,每一个shader编译后都是一个program
        GLES20.glLinkProgram(mProgram);
    }
    
    // 添加draw方法,都是标准化的opengl的操作,不做赘述
    public void draw() {
        // Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram);
        // get handle to vertex shader's vPosition member
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(positionHandle);
        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
                                     GLES20.GL_FLOAT, false,
                                     vertexStride, vertexBuffer);
        // get handle to fragment shader's vColor member
        colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
        // Set color for drawing the triangle
        GLES20.glUniform4fv(colorHandle, 1, color, 0);
        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
        // Disable vertex array
        GLES20.glDisableVertexAttribArray(positionHandle);
  }
}

注意 attribute、uniform、varying的区别,新手容易弄混,参考

(sites.google.com/site/pavelk… "attribute uniform varying区别")

万事俱备,只欠东风,GLSurfaceView调用triangle.draw()。

public void onDrawFrame(GL10 unused) {
    ...

    triangle.draw();
}

实现效果

image.png

有个问题

定义的是等边三角形,为什么渲染出来变形了呢? 原因是,手机宽高比不等于1,一般opengl计算空间是1 * 1 * 1的单位立方体,生成的一帧图像放到手机屏幕上就被拉升变长了。

下一步,我们就来处理屏幕的适配。

三、view/投影变换,适宜屏幕大小

mvp变换

如果你对mvp变换不了解,或者不认识上面这张图,建议先熟悉下,参考learnOpenGL教程

修改Triangle.class

// 修改顶点着色器,增加mvp矩阵
private final String vertexShaderCode =
    // This matrix member variable provides a hook to manipulate
    // the coordinates of the objects that use this vertex shader
    "uniform mat4 uMVPMatrix;" +
    "attribute vec4 vPosition;" +
    "void main() {" +
    
    // the matrix must be included as a modifier of gl_Position
    // Note that the uMVPMatrix factor *must be first* in order
    // for the matrix multiplication product to be correct.
    "  gl_Position = uMVPMatrix * vPosition;" +
    "}";

// Use to access and set the view transformation
private int vPMatrixHandle;

// 修改draw方法,增加顶点变换矩阵 mvpMatrix
public void draw(float[] mvpMatrix) { 
    ...

    // get handle to shape's transformation matrix
    vPMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");
    // Pass the projection and view transformation to the shader
    GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, mvpMatrix, 0);
    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
    // Disable vertex array
    GLES20.glDisableVertexAttribArray(positionHandle);
}   

顺便回顾下glUniformMatrix4fv:

void glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat * value)

通过一致变量(uniform修饰的变量)引用将一致变量值传入渲染管线。
location : uniform的位置。
count : 需要加载数据的数组元素的数量或者需要修改的矩阵的数量。
transpose : 指明矩阵是列优先(column major)矩阵(GL_FALSE)还是行优先(row major)矩阵(GL_TRUE)。
value : 指向由count个元素的数组的指针。

修改GLSurfaceView#MyGLRenderer

增加投影矩阵,projection matrix的设置,保证opengl的视窗和手机window的视窗大小一致,如此缩放后也是等比缩放,不会变形。

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    float ratio = (float) width / height;

    // this projection matrix is applied to object coordinates
    // in the onDrawFrame() method
    // 参数说明
    // 0 projectionMatrix矩阵偏移
    // -ratio ratio -1 1 3 7分别表示左、右、上、下、近平面、远平面
    // 用ratio表示视窗的左、右,即视窗按比例被压瘪了
    Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}

projection 矩阵忘了的,看下图回顾下投影矩阵定义的视锥体,完全不知道的可以跳过:

生成最终的顶点变换矩阵vPMatrix

@Override
public void onDrawFrame(GL10 unused) {
    ...
    // 设置相机位置,即view变换矩阵
    // 参数说明,opengl mvp很基础的概念,此处再详细说明下
    // 0 viewMatrix 偏移
    // (0 0 3)相机位置,模拟人眼的视角。文档里是-3,表示从后面看三角形,比较别扭,影响旋转方向
    // 我这里改成+3,从正面看三角形
    // (0 0 0)相机看的方向,(0 0 0)表示“人眼”注视原点
    // (0 1 0)相机上方与Y轴对齐,表示人脑袋没有左右偏移,为正视方向
    Matrix.setLookAtM(viewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

    // Calculate the projection and view transformation
    Matrix.multiplyMM(vPMatrix, 0, projectionMatrix, 0, viewMatrix, 0);

    // Draw shape
    triangle.draw(vPMatrix);
}

效果:

image.png

四、动画与触摸交互

三角形自动旋转

有了上面的基础,做一个三角形自动旋转就很简单了,直接看代码及注释

Renderer里修改代码:

private float[] rotationMatrix = new float[16];
Override
public void onDrawFrame(GL10 gl) {
    float[] scratch = new float[16];

    ...

    // 创建旋转矩阵,根据time来改变三角形旋转的角度,达到动态效果
    // 一周360°,每次更新0.09°,则需要4000mm转一圈,即周期是4秒
    // 注意,opengl里很多场景下使用弧度制,即π
    long time = SystemClock.uptimeMillis() % 4000L;
    float angle = 0.090f * ((int) time);
    // (0 0 -1.0f)表示围绕-z方向旋转,根据右手定则,为顺时针(大拇指指向-z方向,朝屏幕里)
    Matrix.setRotateM(rotationMatrix, 0, angle, 0, 0, -1.0f);

    // 投影矩阵与旋转矩阵相乘,得到最终的矩阵
    Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0);

    // Draw triangle
    mTriangle.draw(scratch);
}   

最后修改GLSurfaceView里的渲染模式

public MyGLSurfaceView(Context context) extends GLSurfaceView {
    ...
    // 什么都不设置
    
    // 或设置为CONTINUOUSLY,自动更新
   setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}

实现效果

aaaaaa.gif

手势控制旋转

so easy,reading the fucking code!!!

GLSurfaceView中响应touch事件,动态修改mvp矩阵,控制旋转

private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float previousX;
private float previousY;

@Override
public boolean onTouchEvent(MotionEvent e) {

    float x = e.getX();
    float y = e.getY();
    
    // 手势移动的x y距离作为旋转的角度
    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:
          
            float dx = x - previousX;
            float dy = y - previousY;

            // reverse direction of rotation above the mid-line
            // 算法很巧妙,y > height/2时,手势点在下半屏幕,x方向的距离要反过来
            if (y > getHeight() / 2) {
              dx = dx * -1 ;
            }

            // reverse direction of rotation to left of the mid-line
            if (x < getWidth() / 2) {
              dy = dy * -1 ;
            }

            renderer.setAngle(
                    renderer.getAngle() +
                    ((dx + dy) * TOUCH_SCALE_FACTOR));
            // 请求渲染
            requestRender();
    }
    
    // 记录previous坐标
    previousX = x;
    previousY = y;
    return true;
}
    

更改GLSurfaceView的渲染模式

public MyGLSurfaceView(Context context) {
    ...
    // 改成主动渲染,有数据变化时,才通知渲染,性能更好
    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}  

Renderer增加angle的设置,将旋转矩阵中angle替换为手动设置的值,上面自动旋转的angle是随时间动态计算的。

public class MyGLRenderer implements GLSurfaceView.Renderer {
    ...

    public volatile float mAngle;

    public float getAngle() {
        return mAngle;
    }

    public void setAngle(float angle) {
        mAngle = angle;
    }
}

  public void onDrawFrame(GL10 gl) {
      ...
      float[] scratch = new float[16];
      Matrix.setRotateM(rotationMatrix, 0, mAngle, 0, 0, -1.0f);
      Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0);

      // Draw triangle
      mTriangle.draw(scratch);
  } 

实现效果

444.gif

欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵

参考资料

[1] android GL ES官方文档: developer.android.google.cn/guide/topic…

[2] github demo: github.com/summer-go/A…

[3] opengl教程: learnopengl-cn.github.io/

[4] OpenGL ES 与 EGL、GLSL的关系: www.cnblogs.com/yongdaimi/p…

[5] why FloatBuffer: stackoverflow.com/questions/1…

[6] 定义形状: developer.android.google.cn/training/gr…

[7] attribute uniform varying区别: sites.google.com/site/pavelk…

[8] 坐标系统: learnopengl-cn.github.io/01%20Gettin…

[9] 右手坐标系参考图: www.jianshu.com/p/e25bf6dc1…