一、思路
首先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的倍数才行。
六、项目地址
完整代码可见: