阅读 648

[OpenGL]未来视觉6-静态图片纹理加载

大家好,我系苍王。

以下是我这个系列的相关文章,有兴趣可以参考一下,可以给个喜欢或者关注我的文章。

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群

关注下面的标签,发现更多相似文章
评论