书上第四章最后开始介绍使用OpenGL来显示一个2D纹理,其实做音视频2D基本满足绝大多数要求了,下面简单分析一下源码中的流程。
首先我们需要在Java环境中初始化一个SurfaceView,然后在回调中我们传入surface。这里我将AssetsManager也传入Native,因为着色器的文件我是写在Assets中的,我们再Native层进行读取。Android Studio安装GLSL插件之后,编写glsl文件可以有关键字高亮,以及不用像在C语言中那样写大量的换行符。
SurfaceHolder mSurfaceHolder = surfaceView.getHolder();
mSurfaceHolder.addCallback(previewCallback);
...
private Callback previewCallback = new Callback() {
public void surfaceCreated(SurfaceHolder holder) {
pngPreviewController = new PngPreviewController();
pngPreviewController.init(picPath,getAssets(),0);
pngPreviewController.setSurface(holder.getSurface());
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// pngPreviewController.resetSize(width, height);
}
public void surfaceDestroyed(SurfaceHolder holder) {
}
};
接下来先初始化PicPreviewController,以及调用start方法。
void
Java_com_example_yllds_androidopengldemo_PngPreviewController_init(JNIEnv *env, jobject thiz,
jstring picPath,
jobject assetManager,jint type) {
controller = new PicPreviewController(env,assetManager,type);
char* pngFilePath = (char*) env->GetStringUTFChars(picPath, NULL);
LOGI("filepath %s",pngFilePath);
controller->start(pngFilePath);
env->ReleaseStringUTFChars(picPath, pngFilePath);
}
这里做一些初始化,因为demo中我写了一个fbo的测试代码,这里请先将type看做0。接下来就是去assets中读取对应的glsl文件,并存入PicPreviewRender类中。
PicPreviewController::PicPreviewController(JNIEnv *env, jobject assetManager, int type) {
LOGI("VideoDutePlayerController instance created");
pthread_mutex_init(&mLock, nullptr);
pthread_cond_init(&mCondition, nullptr);
screenWidth = 720;
screenHeight = 720;
this->type=type;
char *vertexContent = nullptr;
char *fragContent = nullptr;
if (type == 0) {
readSource(env, "texture/vertex_shader.glsl", "texture/fragment_shader.glsl",
assetManager, vertexContent,
fragContent);
} else {
readSource(env, "fbo/vertex_shader.glsl", "fbo/fragment_shader.glsl", assetManager,
vertexContent,
fragContent);
}
if (!vertexContent || !fragContent) {
LOGI("read source failed");
return;
}
LOGI("glsl content vertex %s", vertexContent);
LOGI("glsl content frag %s", fragContent);
if (type == 0) {
render = new PicPreviewRender(vertexContent, fragContent);
} else {
render = new FboRender(vertexContent, fragContent);
}
decoder = nullptr;
}
然后调用PicPreviewController的start方法开启渲染线程。在这个demo中是用来libpng的库来进行png文件的解码,所以这里也会先初始化解码png的类PngPicDecoder。
bool PicPreviewController::start(char *spriteFilePath) {
LOGI("Creating VideoDutePlayerController thread");
/*send message to render thread to stop rendering*/
decoder = new PngPicDecoder();
decoder->openFile(spriteFilePath);
pthread_create(&_threadId, nullptr, threadStartCallback, this);
return true;
}
循环渲染线程,在窗口设置之后进行egl的初始化,渲染之后会等待,这里将会设计成在窗口有变化的情况下回重新渲染。
oid PicPreviewController::renderLoop() {
bool renderingEnabled = true;
LOGI("renderLoop()");
while (renderingEnabled) {
pthread_mutex_lock(&mLock);
/*process incoming messages*/
switch (_msg) {
case MSG_WINDOW_SET:
initialize();
break;
case MSG_RENDER_LOOP_EXIT:
renderingEnabled = false;
destroy();
break;
default:
break;
}
_msg = MSG_NONE;
if (eglCore) {
eglCore->makeCurrent(previewSurface);
this->drawFrame();
pthread_cond_wait(&mCondition, &mLock);
usleep(100 * 1000);
}
pthread_mutex_unlock(&mLock);
}
LOGI("Render loop exits");
return;
}
最后就是egl环境的初始化了,通过筛选合适的配置,进行egl的初始化,接下来只需要elgMakeCurrent就可以与线程绑定进行渲染了。
bool EGLCore::init(EGLContext sharedContext) {
EGLint numConfigs;
EGLint width;
EGLint height;
EGLint major;//主版本号
EGLint minor;//次版本号
//通过属性去筛选合适的配置
const EGLint attibutes[] = {
EGL_BUFFER_SIZE, 32,
EGL_ALPHA_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //指定渲染api版本 2
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};
if ((display = eglGetDisplay(EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY) {
LOGE("eglGetDisplay() returned error %d", eglGetError());
return false;
}
if (!eglInitialize(display, &major, &minor)) {
LOGE("eglInitialize() returned error %d", eglGetError());
return false;
}
//这里只取一个config
if (!eglChooseConfig(display, attibutes, &config, 1, &numConfigs)) {
LOGE("eglChooseConfig() returned error %d", eglGetError());
release();
return false;
}
EGLint eglContextAttribute[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE
};
if (!(context = eglCreateContext(display, config,
NULL == sharedContext ? EGL_NO_CONTEXT : sharedContext,
eglContextAttribute))) {
LOGE("eglCreateContext() returned error %d", eglGetError());
release();
return false;
}
int *p;
pfneglPresentationTimeANDROID = reinterpret_cast(eglGetProcAddress(
"eglPresentationTimeANDROID"));
if (!pfneglPresentationTimeANDROID) {
LOGE("eglPresentationTimeANDROID is not available!");
}
return true;
}
这一部分资料很多了,简单的说一下把,对glsl语言不了解的同学需要先查一下资料比较好,因为这里比较简单,OpenGl一开始入门学的东西太多,搞得特别混乱,展开了讲又太冗长了。
vertex_shader.glsl 顶点着色器,后面会将对应的坐标通过传入,varying变量将会在插值之后传给片段着色器。
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main(){
gl_Position=position;
v_texcoord=texcoord;
}
fragment_shader.glsl 片段着色器,通过传入的左边以及纹理来进行采样取得最终的颜色值。
varying highp vec2 v_texcoord;
uniform sampler2D yuvTexSampler;
void main(){
gl_FragColor=texture2D(yuvTexSampler,v_texcoord);
}
我们首先初始化shader,创建program。
bool PicPreviewRender::init(int width, int height, PicPreviewTexture *picPreviewTexture) {
this->_backingLeft = 0;
this->_backingTop = 0;
this->_backingWidth = width;
this->_backingHeight = height;
this->picPreviewTexture = picPreviewTexture;
vertShader = 0;
fragShader = 0;
program = 0;
int ret = initShader();
if (ret < 0) {
LOGI("init shader failed...");
this->dealloc();
return false;
}
ret = useProgram();
if (ret < 0) {
LOGI("use program failed...");
this->dealloc();
return false;
}
return true;
}
初始化两个shader并进行编译
int PicPreviewRender::initShader() {
vertShader = compileShader(GL_VERTEX_SHADER, PIC_PREVIEW_VERTEX_SHADER_2);
if (!vertShader) {
return -1;
}
fragShader = compileShader(GL_FRAGMENT_SHADER, PIC_PREVIEW_FRAG_SHADER_2);
if (!fragShader) {
return -1;
}
return 1;
}
GLuint PicPreviewRender::compileShader(GLenum type, const char *source) {
GLint status;
GLuint shader = glCreateShader(type);
if (shader == 0 || shader == GL_INVALID_ENUM) {
LOGE("Failed to create shader %d", type);
return 0;
}
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 0) {
char message[infoLen];
glGetShaderInfoLog(shader, infoLen, NULL, message);
LOGE("Failed to compile shader : %s\n", message);
}
glDeleteShader(shader);
return 0;
}
return shader;
}
连接shader到program中并获取关键变量的位置
int PicPreviewRender::useProgram() {
program = glCreateProgram();
glAttachShader(program, vertShader);
glAttachShader(program, fragShader);
//可以直接调用这个方法进行绑定位置
//或者调用glGetAttribLocation()获取
//或者可以在glsl文件中直接指定(不是特比清楚是不是gles3才有的特性)
glBindAttribLocation(program, ATTRIBUTE_VERTEX, "position");
glBindAttribLocation(program, ATTRIBUTE_TEXCOORD, "texcoord");
glLinkProgram(program);
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
GLint infoLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 0) {
char message[infoLen];
glGetProgramInfoLog(program, infoLen, NULL, message);
LOGE("Error linking program : %s\n", message);
}
glDeleteProgram(program);
return 0;
}
glUseProgram(program);
uniformSampler = glGetUniformLocation(program, "yuvTexSampler");
return 1;
}
既然是要使用纹理,我们先要生成纹理id,这里对纹理配置的参数
int PicPreviewTexture::initTexture() {
glGenTextures(1,&texture);
glBindTexture(GL_TEXTURE_2D,texture);
//设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//将纹理s t轴的坐标都限制在0 - 1 的范围
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D,0);
return 1;
}
接下来将libpng解码出来的rgba像素数据绑定到纹理中
void PicPreviewTexture::updateTexImage(byte *pixel, int width, int height) {
if(pixel){
glBindTexture(GL_TEXTURE_2D,texture);
if(checkGlError("glBindTexture ")){
return;
}
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,pixel);
}
}
准备工作做完之后开始绘制,这个方法我测试了一下读取像素并且输出到png文件,不需要的可以无视。
void PicPreviewRender::render() {
glViewport(_backingLeft, _backingTop, _backingWidth, _backingHeight);
//设置一个颜色状态
glClearColor(0.0f, 0.0f, 1.0f, 0.0f);
//使能颜色状态的值来清屏
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glUseProgram(program);
static const GLfloat _vertices[] = {-1.0f, 1.0f,//左上
-1.0f, -1.0f,//左下
1.0f, 1.0f,//右上
1.0f, -1.0f//右下
};
//stride设置为0自动决定步长,只有在坐标紧密的情况下才行
//设置定点缓存指针
glVertexAttribPointer(ATTRIBUTE_VERTEX, 2, GL_FLOAT, GL_FALSE, 0, _vertices);
glEnableVertexAttribArray(ATTRIBUTE_VERTEX);
//纹理坐标对应
static const GLfloat texCoords[] = {0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f};
//设置纹理缓存指针,varying变量会被插值传入片元着色器
glVertexAttribPointer(ATTRIBUTE_TEXCOORD, 2, GL_FLOAT, 0, 0, texCoords);
glEnableVertexAttribArray(ATTRIBUTE_TEXCOORD);
//绑定纹理
picPreviewTexture->bindTexture(uniformSampler);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//超过glViewport的区域不会报错,但是生成带图片会混乱
//小于则是读取一部分的矩形区域
int width=_backingWidth;
int height=_backingHeight;
unsigned char *buffers = new unsigned char[_backingHeight * _backingWidth * 4];
glReadPixels(0, 0, _backingWidth, _backingHeight, GL_RGBA, GL_UNSIGNED_BYTE, buffers);
LOGI("读取数据成功");
PngWrite pngWrite;
// FILE* fp2=fopen("/mnt/sdcard/ic_launchergrayrgb.rgb","wb");
// fwrite(buffers1,_backingWidth*_backingHeight*4,1,fp2);
// fclose(fp2);
LOGI("start writing png file");
pngWrite.writePngFile("/mnt/sdcard/ic_launchergraynormal.png", width, height,
(buffers));
}
渲染完成之后还需要调用eglSwapBuffers来交换数据,让后台的FrameBuffer切换到前台,显示图像。
if (!eglCore->swapBuffers(previewSurface)) {
LOGE("eglSwapBuffers() returned error %d", eglGetError());
}
跑这个demo的时候注意需要在sd卡根目录放入一张png图片,并且需要手动开启sd卡读写权限。
源码