Android使用OpenGL ES顯示紋理(使用NDK開發)
書上第四章最後開始介紹使用OpenGL來顯示一個2D紋理,其實做音視訊2D基本滿足絕大多數要求了,下面簡單分析一下原始碼中的流程。
EGL環境初始化
首先我們需要在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<PFNEGLPRESENTATIONTIMEANDROIDPROC>(eglGetProcAddress(
"eglPresentationTimeANDROID"));
if (!pfneglPresentationTimeANDROID) {
LOGE("eglPresentationTimeANDROID is not available!");
}
return true;
}
OpenGL ES繪製紋理流程
這一部分資料很多了,簡單的說一下把,對glsl語言不瞭解的同學需要先查一下資料比較好,因為這裡比較簡單,OpenGl一開始入門學的東西太多,搞得特別混亂,展開了講又太冗長了。
1、準備著色器
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);
}
2、編譯顯示卡執行program
我們首先初始化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;
}
3、使用OpenGL的API進行繪製
既然是要使用紋理,我們先要生成紋理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卡讀寫許可權。
原始碼