Bootstrap

Android使用OpenGL 3.0绘制yuv图片示例

一、思路

首先OpenGL是没法直接绘制yuv数据的,所以我们需要在shader中将yuv数据转为rgb数据绘制。

我们可以将yuv数据作为3个不同的纹理传入到片段着色器,然后通过yuv转rgb的公式,得到需要渲染的颜色值。

公式为:( Y~ [0,1] U,V~[-0.5,0.5])

R = Y + 1.4022 * V
G = Y - 0.3456 * U - 0.7145 * V
B = Y + 1.771 * U

二、shader编写

OpenGL 3.0 和 2.0 的 shader 语法存在一些差异,首先我们必须在 shader 脚本中的第一行加上 #version 300 es 声明版本为 3.0。

另外,我们可以通过 layout(location = 0) 直接指定某个变量的 location 值,代替通过使用 glGetUniformLocation 函数获取。

1. 顶点着色器

#version 300 es

layout(location = 0) in vec4 v_Position;
layout(location = 1) in vec2 v_TextureCoord;

out vec2 texture_coord;

void main() {
    gl_Position = v_Position;
    texture_coord = v_TextureCoord;
}

v_Position 是输入的顶点坐标,v_TextureCoord 是输入的纹理坐标,都不需要做额外的处理,直接传入到片段着色器即可。

2. 片段着色器

#version 300 es

precision mediump float;

in vec2 texture_coord;
layout(location = 0) uniform sampler2D sampler_y;
layout(location = 1) uniform sampler2D sampler_u;
layout(location = 2) uniform sampler2D sampler_v;

out vec4 out_color;

void main() {
    float y = texture(sampler_y, texture_coord).x;
    float u = texture(sampler_u, texture_coord).x- 0.5;
    float v = texture(sampler_v, texture_coord).x- 0.5;

    vec3 rgb;
    rgb.r = y + 1.4022 * v;
    rgb.g = y - 0.3456 * u - 0.7145 * v;
    rgb.b = y + 1.771 * u;
    out_color = vec4(rgb, 1);
}

texture 是一个内置函数,用来获取纹理上指定坐标的值。

texture_coord 是顶点着色器的输出变量,out_color 是最终绘制的点的颜色。

三、JavaRenderer 类

实际绘制的类,继承 GLSurfaceView.Renderer 类。

首先,我们在 onSurfaceCreated 回调中完成一些初始化工作,如加载着色器脚本,创建程序,创建纹理,初始化顶点坐标等。

其次,添加一个 setYuvData(byte[] i420, int width, int height) 方法,供外部调用处设置和更新 yuv 的数据,注意纹理u和纹理v的大小只有纹理y的一半。

最后,我们在 onDrawFrame(GL10 gl) 方法中,完成绘制工作。

完整代码如下:

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

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class JavaRenderer implements GLSurfaceView.Renderer {

    private static final String TAG = "JavaRenderer";
    private Context mContext;

    private int mProgram;
    private int[] mTextureIds;

    private int yuvWidth;
    private int yuvHeight;
    private ByteBuffer yBuffer;
    private ByteBuffer uBuffer;
    private ByteBuffer vBuffer;
    protected FloatBuffer mVertexBuffer;

    public JavaRenderer(Context context) {
        mContext = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        init();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // 视距区域设置使用 GLSurfaceView 的宽高
        GLES30.glViewport(0, 0, width, height);
    }

    private void init() {
        String vertexSource = ShaderUtil.loadFromAssets("vertex.vsh", mContext.getResources());
        String fragmentSource = ShaderUtil.loadFromAssets("fragment.fsh", mContext.getResources());
        mProgram = ShaderUtil.createProgram(vertexSource, fragmentSource);
        //创建纹理
        mTextureIds = new int[3];
        GLES30.glGenTextures(mTextureIds.length, mTextureIds, 0);
        for (int i = 0; i < mTextureIds.length; i++) {
            //绑定纹理
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureIds[i]);
            //设置环绕和过滤方式
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT);
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT);
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR);
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
        }

        // OpenGL的世界坐标系是 [-1, -1, 1, 1],纹理的坐标系为 [0, 0, 1, 1]
        float[] vertices = new float[]{
                // 前三个数字为顶点坐标(x, y, z),后两个数字为纹理坐标(s, t)
                // 第一个三角形
                1f,  1f,  0f,       1f, 0f,
                1f,  -1f, 0f,       1f, 1f,
                -1f, -1f, 0f,       0f, 1f,
                // 第二个三角形
                1f,  1f,  0f,       1f, 0f,
                -1f, -1f, 0f,       0f, 1f,
                -1f, 1f,  0f,       0f, 0f
        };
        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4); // 一个 float 是四个字节
        vbb.order(ByteOrder.nativeOrder()); // 必须要是 native order
        mVertexBuffer = vbb.asFloatBuffer();
        mVertexBuffer.put(vertices);
    }

    public void setYuvData(byte[] i420, int width, int height) {
        if (yBuffer != null) yBuffer.clear();
        if (uBuffer != null) uBuffer.clear();
        if (vBuffer != null) vBuffer.clear();

        // 该函数多次被调用的时,不要每次都new,可以设置为全局变量缓存起来
        byte[] y = new byte[width * height];
        byte[] u = new byte[width * height / 4];
        byte[] v = new byte[width * height / 4];
        System.arraycopy(i420, 0, y, 0, y.length);
        System.arraycopy(i420, y.length, u, 0, u.length);
        System.arraycopy(i420, y.length + u.length, v, 0, v.length);
        yBuffer = ByteBuffer.wrap(y);
        uBuffer = ByteBuffer.wrap(u);
        vBuffer = ByteBuffer.wrap(v);
        yuvWidth = width;
        yuvHeight = height;
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        if (yBuffer == null || uBuffer == null || vBuffer == null) {
            return;
        }
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); // clear color buffer
        // 1. 选择使用的程序
        GLES30.glUseProgram(mProgram);
        // 2.1 加载纹理y
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //激活纹理0
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureIds[0]); //绑定纹理
        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_LUMINANCE, yuvWidth,
                yuvHeight, 0, GLES30.GL_LUMINANCE, GLES30.GL_UNSIGNED_BYTE, yBuffer); // 赋值
        GLES30.glUniform1i(0, 0); // sampler_y的location=0, 把纹理0赋值给sampler_y
        // 2.2 加载纹理u
        GLES30.glActiveTexture(GLES30.GL_TEXTURE1);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureIds[1]);
        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_LUMINANCE, yuvWidth / 2,
                yuvHeight / 2, 0, GLES30.GL_LUMINANCE, GLES30.GL_UNSIGNED_BYTE, uBuffer);
        GLES30.glUniform1i(1, 1); // sampler_u的location=1, 把纹理1赋值给sampler_u
        // 2.3 加载纹理v
        GLES30.glActiveTexture(GLES30.GL_TEXTURE2);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureIds[2]);
        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_LUMINANCE, yuvWidth / 2,
                yuvHeight / 2, 0, GLES30.GL_LUMINANCE, GLES30.GL_UNSIGNED_BYTE, vBuffer);
        GLES30.glUniform1i(2, 2); // sampler_v的location=2, 把纹理1赋值给sampler_v
        // 3. 加载顶点数据
        mVertexBuffer.position(0);
        GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 5 * 4, mVertexBuffer);
        GLES30.glEnableVertexAttribArray(0);
        mVertexBuffer.position(3);
        GLES30.glVertexAttribPointer(1, 2, GLES30.GL_FLOAT, false, 5 * 4, mVertexBuffer);
        GLES30.glEnableVertexAttribArray(1);
        // 4. 绘制
        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 6);
    }

}

用到一个 ShaderUtil 工具类,代码如下:

import android.content.res.Resources;
import android.opengl.GLES30;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;

public class ShaderUtil {

    public static final String TAG = "ShaderUtils";

    public static int loadShader(int type, String source) {
        // 1. create shader
        int shader = GLES30.glCreateShader(type);
        if (shader == GLES30.GL_NONE) {
            Log.e(TAG, "create shared failed! type: " + type);
            return GLES30.GL_NONE;
        }
        // 2. load shader source
        GLES30.glShaderSource(shader, source);
        // 3. compile shared source
        GLES30.glCompileShader(shader);
        // 4. check compile status
        int[] compiled = new int[1];
        GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == GLES30.GL_FALSE) { // compile failed
            Log.e(TAG, "Error compiling shader. type: " + type + ":");
            Log.e(TAG, GLES30.glGetShaderInfoLog(shader));
            GLES30.glDeleteShader(shader); // delete shader
            shader = GLES30.GL_NONE;
        }
        return shader;
    }

    public static int createProgram(String vertexSource, String fragmentSource) {
        // 1. load shader
        int vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == GLES30.GL_NONE) {
            Log.e(TAG, "load vertex shader failed! ");
            return GLES30.GL_NONE;
        }
        int fragmentShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource);
        if (fragmentShader == GLES30.GL_NONE) {
            Log.e(TAG, "load fragment shader failed! ");
            return GLES30.GL_NONE;
        }
        // 2. create gl program
        int program = GLES30.glCreateProgram();
        if (program == GLES30.GL_NONE) {
            Log.e(TAG, "create program failed! ");
            return GLES30.GL_NONE;
        }
        // 3. attach shader
        GLES30.glAttachShader(program, vertexShader);
        GLES30.glAttachShader(program, fragmentShader);
        // we can delete shader after attach
        GLES30.glDeleteShader(vertexShader);
        GLES30.glDeleteShader(fragmentShader);
        // 4. link program
        GLES30.glLinkProgram(program);
        // 5. check link status
        int[] linkStatus = new int[1];
        GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0);
        if (linkStatus[0] == GLES30.GL_FALSE) { // link failed
            Log.e(TAG, "Error link program: ");
            Log.e(TAG, GLES30.glGetProgramInfoLog(program));
            GLES30.glDeleteProgram(program); // delete program
            return GLES30.GL_NONE;
        }
        return program;
    }

    public static String loadFromAssets(String fileName, Resources resources) {
        String result = null;
        try {
            InputStream is = resources.getAssets().open(fileName);
            int length = is.available();
            byte[] data = new byte[length];
            is.read(data);
            is.close();
            result = new String(data, "UTF-8");
            result.replace("\\r\\n", "\\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }
}

四、调用场景

使用上面的 JavaRenderer,并使用 assets 目录下的 yuv 数据模拟一下。

import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private GLSurfaceView mGlSurfaceView;
    private JavaRenderer mRenderer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!checkOpenGLES30()) {
            Log.e(TAG, "con't support OpenGL ES 3.0!");
            finish();
        }
        mGlSurfaceView = new GLSurfaceView(this);
        mGlSurfaceView.setEGLContextClientVersion(3); // 设置OpenGL版本号
        mRenderer = new JavaRenderer(this);
        mGlSurfaceView.setRenderer(mRenderer);
        mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 设置渲染模式为仅当手动执行requestRender时才绘制
        setContentView(mGlSurfaceView);
        // 实际场景中,可能是在相机预览回调或解码回调中调用,这里仅使用预设的yuv图片做示例
        drawYuv();
    }

    private void drawYuv() {
        // 绘制的width必须是8的倍数,height必须是2的倍数,如果不是则需要对齐到8的倍数,否则渲染的结果不对
        byte[] i420 = FileUtil.getAssertData(this, "408x720_i420.yuv");
        mRenderer.setYuvData(i420, 408, 720);
//        byte[] i420 = FileUtil.getAssertData(this, "204x360_i420.yuv");
//        mRenderer.setYuvData(i420, 204, 360);
        mGlSurfaceView.requestRender(); // 手动触发渲染
    }

    private boolean checkOpenGLES30() {
        ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        ConfigurationInfo info = am.getDeviceConfigurationInfo();
        return (info.reqGlEsVersion >= 0x30000);
    }

    @Override
    protected void onPause() {
        mGlSurfaceView.onPause();
        super.onPause();
    }

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

五、执行结果

使用 408x720_i420.yuv 图片执行的截图如下:
在这里插入图片描述

使用 204x360_i420.yuv 图片执行的截图如下:
在这里插入图片描述

可以看到结果出现错误了,之前项目中遇到了这个bug,定位后发现绘制的width必须是8的倍数才行。

六、项目地址

完整代码可见:

https://github.com/afei-cn/OpenGLSample/tree/master/yuvdemo

;