[OpenGL]未來視覺6-靜態圖片紋理載入

Cang_Wang發表於2019-04-16

大家好,我係蒼王。

以下是我這個系列的相關文章,有興趣可以參考一下,可以給個喜歡或者關注我的文章。

OpenGL和音視訊相關的文章,將會在 [OpenGL]未來視覺-MagicCamera3實用開源庫 當中給大家呈現 裡面會記錄我編寫這個庫的一些經歷和經驗。

這一章是寫圖片載入。

你用Android上開發,如果想直接載入一個圖片會怎麼做?

1.直接使用一個ImageView載入圖片?

2.使用一個view的onDraw來繪製一個Bitmap圖片資料?

這裡ImageView其實在原始碼裡都是通過onDraw方法使用canvas畫圖片(Drawable物件,並不是bitmap) 而Canvas物件實現在底層中使用了skcanvas庫,用於將圖片轉換到底層硬體可以顯示的資料。

下面一個簡單的ImageView原始碼分析ImageView核心原始碼分析

你需要知道幾點

1.ImageView的canvas是通過繪製Drawable物件繪製,並不是bitmap

2.scaleType等轉換,是通過java內建Matrix矩陣函式做轉換的,最終通過canvas設定matrix矩陣

為什麼你的canvas那麼慢?淺析Android的canvas效能

這是canvas繪製的原理的一些分析

1.canvas是實用skia庫來繪製的,實用cpu計算繪製

2.Android普通的view都是繼承於GLES20RecordingCanvas,這個類繪製圖都帶有硬體加速

3.SurfaceView TextureView裡面的Canvas並不是 GLES20RecordingCanvas,所以要特別注意,但是其實用紋理渲染是實用GPU的。

4.從Android繪製效率上說,硬體加速繪製>opengl繪製>canvas繪製
自定義view筆記-之關於硬體加速

這篇是關於硬體加速的一些淺析

1.view無法強制開啟硬體加速,只能強制關閉。

2.並不是view的繪製操作都支援硬體加速

那還有其他方式繪製圖片嗎?如果我要在圖片中加入一些濾鏡效果應該怎麼做? 我們可以使用SurfaceView、TextureView或者GLSurfaceView來繪製圖片,他們都會持有canvas物件,而且這些物件都是非硬體加速的。同時他們都可以自定義使用opengles來繪製。

我們上面說過普通的view的canvas都是使用GLES20RecordingCanvas來繪製,從名字上來看就知道是用opengles2.0版本來做硬體加速的轉換編寫了。

那麼我們也是可以通過這些view來自定義載入Opengles的。之前已經介紹過MagicCamera3中相機是使用SurfaceView+Opengles的紋理載入方式來編寫的。

GLSurfaceView其本身就自帶了GLThread並初始化了EGL環境,系統預設mode==RENDERMODE_CONTINUOUSLY,這樣系統會自動重繪;mode==RENDERMODE_WHEN_DIRTY時,只有surfaceCreate的時候會繪製一次,然後就需要通過requestRender()方法主動請求重繪。同時也提到,如果你的介面不需要頻繁的重新整理最好是設定成RENDERMODE_WHEN_DIRTY,這樣可以降低CPU和GPU的活動,可以省電。

而SurfaceView你只會在觸發的時候繪製一次,沒有模式可以切換。GLSurfaceView是繼承於SurfaceView。

下面就說一下怎麼使用SurfaceView來繪製一個紋理圖片

首先是要初始化Opengles,和之前介紹的攝像頭的opengl的初始化類似,但是要傳入surface物件,assets物件,圖片的地址,以及圖片的角度。

private fun initOpenGL(surface: Surface){
        mExecutor.execute {
            //傳入surface物件,assets物件,圖片地址和圖片角度
            val textureId = OpenGLJniLib.magicImageFilterCreate(surface,BaseApplication.context.assets,imagePath,ExifUtil.getExifOrientation(imagePath))
            if (textureId < 0){
                Log.e(TAG, "surfaceCreated init OpenGL ES failed!")
                return@execute
            }
            mSurfaceTexture = SurfaceTexture(textureId)
            //如果幀圖有改變就畫圖
            mSurfaceTexture?.setOnFrameAvailableListener {
                //畫圖
                drawOpenGL()
            }
        }
    }
複製程式碼

這裡如果你有辦法使用C++讀取到圖片資料頭Exif資料,最好還是使用C++來做,這邊因為網上找了很久都沒有能簡單使用C++讀取exif資料的方法,故在討巧的使用java層解析讀取好,然後再傳入native,至於角度有什麼作用,就看後面的解析吧。

//圖片濾鏡surfaceView初始化的時候建立
JNIEXPORT jint JNICALL
Java_com_cangwang_magic_util_OpenGLJniLib_magicImageFilterCreate(JNIEnv *env, jobject obj,
                                                            jobject surface,jobject assetManager,jstring imgPath,jint degree) {
    std::unique_lock<std::mutex> lock(gMutex);
    if(glImageFilter){ //停止攝像頭採集並銷燬
        glImageFilter->stop();
        delete glImageFilter;
        glImageFilter = nullptr;
    }

    //初始化native window
    ANativeWindow *window = ANativeWindow_fromSurface(env,surface);
    //初始化app內獲取資料管理
    aAssetManager= AAssetManager_fromJava(env,assetManager);
    //初始化圖片 jstring轉為std::string
    const char* addressStr = env->GetStringUTFChars(imgPath,0);
    std::string nativeAddress = addressStr;
    
    glImageFilter = new ImageFilter(window,aAssetManager,nativeAddress,degree);
    env->ReleaseStringUTFChars(imgPath, addressStr);
    //建立
    return glImageFilter->create();
}
複製程式碼

初始化時還需要設定圖片角度

void ImageFilter::setFilter(AAssetManager* assetManager) {
    if(filter != nullptr){
        filter->destroy();
    }
    filter = new MagicNoneFilter(assetManager);
    filter->setPool(pool);
    //調整濾鏡中的圖片的方向問題
    filter->setOrientation(degree);
    ALOGD("set filter success");
}
void GPUImageFilter::setOrientation(int degree) {
    this->degree = degree;
    //獲取繪製時需要的角度變換,這裡只是相容圖片0,90,180,270度
    mGLTextureBuffer = getRotation(degree, false, false);
}
複製程式碼

相容角度計算,這個是在shader載入的時候需要調整角度的

//獲取角度
float* getRotation(int degree, const bool flipHorizontal, const bool flipVertical){
    const float* rotateTex;
    //調整角度
    switch (degree){
        case 90:
            rotateTex = TEXTURE_ROTATED_90;
            break;
        case 180:
            rotateTex = TEXTURE_ROTATED_180;
            break;
        case 270:
            rotateTex = TEXTURE_ROTATED_270;
            break;
        case 0:
        default:
            rotateTex = TEXTURE_NO_ROTATION;
            break;
    }
    //垂直翻轉
    if (flipHorizontal){
        const static float flipTran[]={
                flip(rotateTex[0]),rotateTex[1],
                flip(rotateTex[2]),rotateTex[3],
                flip(rotateTex[4]),rotateTex[5],
                flip(rotateTex[6]),rotateTex[7]
        };
        return const_cast<float *>(flipTran);
    }

    //水平翻轉
    if (flipVertical){
        const static float flipTran[]={
                rotateTex[0],flip(rotateTex[1]),
                rotateTex[2],flip(rotateTex[3]),
                rotateTex[4],flip(rotateTex[5]),
                rotateTex[6],flip(rotateTex[7])
        };
        return const_cast<float *>(flipTran);
    }

    return const_cast<float *>(rotateTex);
}

複製程式碼

建立紋理

int ImageFilter::create() {
    //初始化,清空視口顏色
    glDisable(GL_DITHER);
    glClearColor(0,0,0,0);
    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

    //建立EGL環境
    if (!mEGLCore->buildContext(mWindow,EGL_NO_CONTEXT)){
        return -1;
    }

    //圖片初始化
    if (imageInput!= nullptr){
        imageInput->init();
    }

    //濾鏡初始化
    if (filter!= nullptr)
        filter->init();
    //獲取紋理id
    mTextureId = get2DTextureID();
    ALOGD("get textureId success");

    return mTextureId;
}
複製程式碼

輸入視口大小,這裡需要設定顯示圖片顯示尺寸,以及螢幕尺寸

void ImageFilter::change(int width, int height) {
    //設定視口
    glViewport(0,0,width,height);
    this->mScreenWidth = width;
    this->mScreenHeight = height;
    if (imageInput!= nullptr){
        //觸發輸入大小更新
        imageInput->onInputSizeChanged(width, height);

        //初始化圖片幀緩衝
        imageInput->initFrameBuffer(imageInput->mImageWidth,imageInput->mImageHeight);

        if (filter != nullptr){
            //設定濾鏡寬高
            filter->onInputSizeChanged(width,height);
            //設定圖片的寬高
            filter->onInputDisplaySizeChanged(imageInput->mImageWidth,imageInput->mImageHeight);
            //設定矩陣
            setMatrix(width,height);
            //初始化濾鏡幀緩衝
            filter->initFrameBuffer(imageInput->mImageWidth,imageInput->mImageHeight);
        } else{
            //銷燬圖片幀緩衝
            imageInput->destroyFrameBuffers();
        }
    }
}
複製程式碼

這個網上找的時候,網上圖片是以0度為標準,可以用以下程式碼來顯示。通過正交投影很簡單就能完成。

 public void onSurfaceChanged(GL10 glUnused, int width, int height) {
        // Set the OpenGL viewport to fill the entire surface.
        glViewport(0, 0, width, height);

        final float aspectRatio = width > height ? 
            (float) width / (float) height : 
            (float) height / (float) width;

        if (width > height) {
            // Landscape
            orthoM(projectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f);
        } else {
            // Portrait or square
            orthoM(projectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f);
        }   
    }
複製程式碼

但是圖片的角度會對圖片大小顯示比例會有影響,如果調整不正確,顯示會問題非常凸顯,這裡就只區分90和270,還得通過螢幕尺寸、圖片角度、圖片尺寸來計算出正交矩陣,有些相機拍照後儲存的圖片是偏移這兩種角度的。這裡螢幕一直是豎屏方向,還沒測試過橫屏。

void ImageFilter::setMatrix(int width,int height){
    memcpy(mvpMatrix,NONE_MATRIX,16);
   
    if (degree == 90 || degree == 270){  //先判斷角度
        float x;
        if(imageInput->mImageHeight>imageInput->mImageWidth){  //圖片寬比高要大 ,螢幕寬/螢幕高 * 螢幕高/螢幕寬
            x = width / (float) height *
                (float) imageInput->mImageHeight / imageInput->mImageWidth;
        } else{ //圖片寬比高要大 ,螢幕高/螢幕寬 * 螢幕高/螢幕寬
            x = height / (float) width
                    * (float) imageInput->mImageHeight / imageInput->mImageWidth;
        }
        ALOGD("x=%f",x);
        orthoM(mvpMatrix, 0, -1, 1, -x, x, -1, 1);
    } else{  //圖片高比寬要大 ,螢幕寬/螢幕高 * 螢幕高/螢幕寬
        float y;
        if(imageInput->mImageHeight>imageInput->mImageWidth){
            y = width / (float) height *
                (float) imageInput->mImageHeight / imageInput->mImageWidth;
        } else{ //圖片高比寬要大 ,螢幕高/螢幕寬 * 螢幕寬/螢幕高
            y = height / (float) width
                * ((float) imageInput->mImageWidth / imageInput->mImageHeight);
        }
        ALOGD("y=%f",y);
        orthoM(mvpMatrix, 0, -1, 1, -y, y, -1, 1);
    }
    filter->setMvpMatrix(mvpMatrix);
}
複製程式碼

這裡計算後顯示到螢幕的尺寸是正常的。通過正交矩陣來做縮放比例,視口還是螢幕尺寸。

這種載入比螢幕大很多的圖片的時候,會需要一定的延遲,因為解析成紋理也是需要時間的。

經過計算使用stb_image來載入3840*2160的圖片,小米6上耗時700毫秒以上,那麼首次顯示到螢幕上會黑屏一下。

如果大家有優化的方法可以告訴我這邊,我也繼續試驗完善。

新建一個專欄群,希望有興趣的同學多多討論。

客戶端音視訊Opengles群

相關文章