OpenGL.Shader:2 - Android Cpp下加载assets图片资源 / 各种格式加载纹理 / 在线找AndroidNative源码

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a360940265a/article/details/88880831

OpenGL.Shader:2 - Android Cpp下读取assets图片资源 / 读取图片加载纹理 / 在线找AndroidNative源码

(AS3.x rebuild出现More than one file was found with OS independent path)

这篇文章主要解决标题上的几个大问题。为啥是说大难题?据我发现,第一个问题(Cpp下读取assets图片资源),全网没有一篇文章能很好的说清楚整个流程;第二个问题(Cpp下读取各种格式图片加载纹理)网上很多都是不实际的方法。为啥说不实际,java层利用系统的bitmap.API方法操作资源对象,然后再通过jni传入?商用App这样做性能效率能不低?第三个问题(Cpp下操作三大矩阵)这个比较好理解,但我还是不见有完整的跨平台封装贡献出来。也通过这个问题简单介绍怎么在线找AndroidNative的源码。

OpenGL.Shader:1为基础,结合之前OpenGL.ES在Android上的简单实践:11-全景(索引-深度测试)索引知识的内容,在纯Cpp层渲染出一个正方体,并在每个正方体上贴上纹理。

首先贴上GLRender的实现类NativeGLRender的整体代码,有个大概认识:

NativeGLRender::NativeGLRender() {
    init(); // 初始化非GL-Object
}

void NativeGLRender::init() {
    mEglCore = NULL;
    mWindowSurface = NULL;
    res_path = NULL;
    viewMatrix = new float[16];
    CELLMath::Matrix::setIdentityM(viewMatrix, 0);
    projectionMatrix = new float[16];
    CELLMath::Matrix::setIdentityM(projectionMatrix, 0);
    // 投影矩阵P x 视图矩阵V = VP
    viewProjectionMatrix = new float[16];
    memset(viewProjectionMatrix, 0, sizeof(float) * 16);
    // (投影矩阵P x 视图矩阵V) x 具体模型的模型矩阵M = MVP
    modelViewProjectionMatrix = new float[16];
    memset(modelViewProjectionMatrix, 0, sizeof(float) * 16);
}

NativeGLRender::~NativeGLRender() {
    // 回收各种 非 GL-Object
    if(viewMatrix!=NULL) {
        delete [] viewMatrix;
        viewMatrix = NULL;
    }
    if(projectionMatrix!=NULL) {
        delete [] projectionMatrix;
        projectionMatrix = NULL;
    }
    if(viewProjectionMatrix!=NULL) {
        delete [] viewProjectionMatrix;
        viewProjectionMatrix = NULL;
    }
    if(modelViewProjectionMatrix!=NULL) {
        delete [] modelViewProjectionMatrix;
        modelViewProjectionMatrix = NULL;
    }
    if(res_path!=NULL) {
        delete res_path;
        res_path = NULL;
    }
}

void NativeGLRender::setResPath(char *string) {
    res_path = new char[250];
    strcpy(res_path, string);
    LOGI("setResPath : %s\n", res_path);
}

void NativeGLRender::surfaceCreated(ANativeWindow *window)
{
    if (mEglCore == NULL) {
        mEglCore = new EglCore(NULL, FLAG_RECORDABLE);
    }
    mWindowSurface = new WindowSurface(mEglCore, window, true);
    assert(mWindowSurface != NULL && mEglCore != NULL);
    LOGD("render surface create ... ");
    mWindowSurface->makeCurrent();

    cube = new CubeIndex();
    cubeShaderProgram = new CubeShaderProgram();

    char _res_name[250]={0};
    sprintf(_res_name, "%s%s", res_path, "test.jpg");
    // 输入资源文件的资源路径,创建纹理,返回纹理id
    animation_texure = TextureHelper::createTextureFromImage(_res_name);
}
void NativeGLRender::surfaceChanged(int width, int height)
{
    mWindowSurface->makeCurrent();
    LOGD("render surface change ... update MVP!");
    glViewport(0,0, width, height);
    glEnable(GL_DEPTH_TEST);

    CELLMath::Matrix::perspectiveM(projectionMatrix, 45.0f, (float)width/(float)height, 1.0f, 100.0f);
    CELLMath::Matrix::setLookAtM(viewMatrix, 0,
                             4.0f, 4.0f, 4.0f,
                             0.0f, 0.0f, 0.0f,
                             0.0f, 1.0f, 0.0f);
    CELLMath::Matrix::multiplyMM(viewProjectionMatrix,  projectionMatrix, viewMatrix);

    mWindowSurface->swapBuffers();
}
void NativeGLRender::renderOnDraw()
{
    if (mEglCore == NULL) {
        LOGW("Skipping drawFrame after shutdown");
        return;
    }
    mWindowSurface->makeCurrent();
    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
    cubeShaderProgram->ShaderProgram::userProgram();
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, animation_texure);
    glUniform1i(cubeShaderProgram->uTextureUnit, 0);
    CELLMath::Matrix::multiplyMM(modelViewProjectionMatrix, viewProjectionMatrix, cube->modelMatrix);
    cubeShaderProgram->setUniforms(modelViewProjectionMatrix);
    cube->bindData(cubeShaderProgram);
    cube->draw();
    mWindowSurface->swapBuffers();
}
void NativeGLRender::surfaceDestroyed(void)
{
    // 清空自定义模型,纹理,各种 GL-Object
    if(cubeShaderProgram!=NULL) {
        delete cubeShaderProgram;
        cubeShaderProgram = NULL;
    }
    if(cube!=NULL) {
        delete cube;
        cube = NULL;
    }
    if (mWindowSurface) {
        mWindowSurface->release();
        delete mWindowSurface;
        mWindowSurface = NULL;
    }
    if (mEglCore) {
        mEglCore->release();
        delete mEglCore;
        mEglCore = NULL;
    }
}

Cube和CubeShaderProgram在之前的OpenGL.ES在Android上的简单实践:11-全景(索引-深度测试)有具体的介绍,这里就不再论述。大致的流程也不难懂,在NativeGLRender构造函数初始化非GL-Object,在三大回调处理GL的相关逻辑。

1、借助libzip解压apk,释放assets资源文件

资源文件放在assets目录下,在工程打包成apk的时候,就会保持原来格式,不会被进行压缩。在Java层我们可以通过Context.getAssets()获取AssetManager进行操作。NDK也有对应的头文件#include <android/asset_manager.h>和<android/asset_manager_jni.h>!但是!But!However,借助Assets读取的资源文件,是拿不到文件的路径。而且如果想对文件进行一些操作,都需要读取文件内容到运行的内存当中。这样的操作缺点太明显了,也不够自由度。那怎么办呢?

那么为什么Assets的系统API都能读取文件内容了,何为就不提供文件的路径和其他信息呢?其实是因为Assets系统API是直接读取apk包里面的assets文件,并没有解压缩,所以自然就没有具体的路径啦。那么能不能骚操作一把,我们自己解压apk获取assets资源文件并释放到磁盘空间? 借助 libzip.so 可以实现这一步。

首先在NaitveEGL.java 新增两个方法:

public class NativeEGL {
    static {
        System.loadLibrary("zip");
        System.loadLibrary("native-egl");
    }
    private Context ctx;
    public NativeEGL(Context context) {
        ctx = context;
        initBeforeEGL(ctx);
    }
    public String getPackageResourceAPK() {
        return ctx.getPackageResourcePath();
        // /data/app/org.zzrblog.nativecpp-zzIu0MPPws9Df9SN-U1BRA==/base.apk
        // 类似这种以根目录开头的,apk后缀的压缩文件,然后再在JNI层用zip库解压缩。
        // 解压出 assets/mipmap/filename到getResourceCacheDir()相应的目录下
    }
    public String getResourceCacheDir() {
        // /storage/emulated/0/Android/data/org.zzrblog.nativecpp/cache
        File externalCacheDir = ctx.getExternalCacheDir();
        assert externalCacheDir != null;
        return externalCacheDir.getAbsolutePath();
    }
    public native void initBeforeEGL(Context context);
    // ... ...
}

其中getPackageResourcePath()方法比较关键,这个就是返回资源压缩包base.apk的所在路径。准备这两个方法是提供native void initBeforeEGL使用的。现在我们就进入initBeforeEGL方法。

extern "C"
JNIEXPORT void JNICALL
Java_org_zzrblog_nativecpp_NativeEGL_initBeforeEGL(JNIEnv *env, jobject instance, jobject context)
{
    jclass j_native_egl_class = env->GetObjectClass(instance);
    jmethodID getResAbsolutePath_mid = env->GetMethodID(j_native_egl_class, "getPackageResourceAPK",
                                                        "()Ljava/lang/String;");
    jstring compressed_apk_path_jstr = static_cast<jstring>(env->CallObjectMethod(instance, getResAbsolutePath_mid));
    const char *compressed_apk_path_cstr = env->GetStringUTFChars(compressed_apk_path_jstr, GL_FALSE);
    LOGI("资源压缩apk文件:%s", compressed_apk_path_cstr);
    jmethodID getResourceCacheDir_mid = env->GetMethodID(j_native_egl_class, "getResourceCacheDir",
                                                        "()Ljava/lang/String;");
    jstring release_res_path_jstr = static_cast<jstring>(env->CallObjectMethod(instance, getResourceCacheDir_mid));
    const char *release_res_path_cstr = env->GetStringUTFChars(release_res_path_jstr, GL_FALSE);
    LOGI("本地解压缩路径:%s", release_res_path_cstr);
    // 解压纹理资源文件
    unzipAssetsResFile(compressed_apk_path_cstr, release_res_path_cstr);

    renderer = new NativeGLRender();
    char res_path[250]={0};
    strcat (res_path, release_res_path_cstr);
    strcat (res_path, "/");
    strcat (res_path, "assets/mipmap/");
    renderer->setResPath(res_path);

    env->ReleaseStringUTFChars(compressed_apk_path_jstr, compressed_apk_path_cstr);
    env->ReleaseStringUTFChars(release_res_path_jstr, release_res_path_cstr);
}
void unzipAssetsResFile(const char *compressed_apk_path_cstr, const char *release_res_path_cstr)
{
    int iErr = 0;
    struct zip* apkArchive = zip_open(compressed_apk_path_cstr, ZIP_CHECKCONS, &iErr);
    if(!apkArchive) {
        LOGE("zip open failed:%d\n", iErr);
        return;
    }
    struct zip_stat fstat;
    zip_stat_init(&fstat);

    int numFiles = zip_get_num_files(apkArchive);
    // 指定要解压出"assets/mipmap/"这类前缀的文件
    const char *prefix = "assets/mipmap/";
    for (int i=0; i<numFiles; i++) {
        const char* name = zip_get_name(apkArchive, i, 0);
        if (name == NULL) {
            LOGE("Error reading zip file name at index %i : %s", i, zip_strerror(apkArchive));
            return;
        }
        zip_stat(apkArchive,name,0,&fstat);
        //LOGD("Index %i:%s,Uncompressed Size:%d,Compressed Size:%d", i, fstat.name, fstat.size, fstat.comp_size);
        // 查找指定前缀的资源文件
        const char * ret = strstr(fstat.name, prefix);
        if( ret ) {
            LOGI("find it! : %s",ret);
            char dest_path[250]={0};
            strcat (dest_path, release_res_path_cstr);
            strcat (dest_path, "/");
            strcat (dest_path, prefix);
            // create all level path
            int iPathLength=strlen(dest_path);
            int iLeaveLength=0;
            int iCreatedLength=0;
            char szPathTemp[250]={0};
            for (int j=0; (NULL!=strchr(dest_path+iCreatedLength,'/')); j++)
            {
                iLeaveLength = strlen(strchr(dest_path+iCreatedLength,'/'))-1;
                iCreatedLength = iPathLength - iLeaveLength;
                strncpy(szPathTemp, dest_path, iCreatedLength);
                if (access(szPathTemp, F_OK) != 0){
                    mkdir(szPathTemp, 0777);
                    LOGI("mkdir : %s\n", szPathTemp);
                }
            }
            if (access(dest_path, F_OK) != 0){
                mkdir(dest_path, 0777);
                LOGI("mkdir : %s\n", dest_path);
            }
            // unzip file
            char dest_file[250]={0};
            strcat (dest_file, release_res_path_cstr);
            strcat (dest_file, "/");
            strcat (dest_file, fstat.name);
            //LOGI("uncompressed file : %s\n", dest_file);
            if(access(dest_file, F_OK) == 0) {
                LOGI("unzip %s has exist.\n", dest_file);
                continue;
            }
            FILE *fp = fopen(dest_file, "w+");
            if (!fp) {
                LOGE("Create unzip file failed.");
                break;
            }

            ssize_t iRead = 0;
            size_t iLen = 0;
            char buf[1024];
            struct zip_file* file = zip_fopen(apkArchive, fstat.name, 0);
            while(iLen < fstat.size)
            {
                iRead = zip_fread(file, buf, 1024);
                if (iRead < 0) {
                    LOGE("zip_fread file failed");
                    break;
                }
                fwrite(buf, 1, static_cast<size_t>(iRead), fp);
                iLen += iRead;
            }
            fclose(fp);
            LOGI("Create unzip file : %s done!", dest_file);
            zip_fclose(file);
        }
    }
    zip_close(apkArchive);
}

unzipAssetsResFile方法主要流程是:

1、zip_open打开压缩文件apkArchive,zip_stat_init初始化状态结构体zip_stat 。
2、zip_get_num_files获取当前压缩文件包含的所有文件索引数目,并进行for循环遍历。
3、for循环 -> zip_get_name获取当前索引的文件名,zip_stat(apkArchive,name,0,&zip_stat); 获取当前索引文件的状态信息,通过状态结构体zip_stat,可以获取到当前索引文件的详尽信息。(文件名,索引号,crc,压缩前后的大小,压缩方式、加密方式)
4、找出符合prefix = "assets/mipmap/"的文件子项,并指定解压路径是getResourceCacheDir下的assets/mipmap/,检测是否存在当前路径,没有则需要创建目录路径。
5、zip_fopen(apkArchive, fstat.name, 0)打开索引文件,读取文件内容;fopen打开解压的目录文件路径,把读取到的文件内容写日目标文件。解压完成。
6、zip_fclose 关闭当前操作的索引文件,zip_close关闭当前压缩文件。

经过unzipAssetsRefFile方法之后,我们就把工程目录的assets/mipmap,解压到/storage/emulated/0/Android/data/org.zzrblog.nativecpp/cache/assets/mipmap/ filename,此后我们就可以自由的操作文件资源了。(按实际需要解压这个base.apk里面的文件,也可以打开调试日志看看里面实际有多少个文件)

2、读取图片文件(开源,跨平台)

现在我们已经能自由的操作图片资源了,但是图片的格式种类繁多,需要解格式之后获取位图数据,这个是一个贼麻烦的操作。而且网上太多可用库了,又分不同的平台,真的太繁琐了。所以这里贡献一下歪果人整理的读取图片库,以源码的形式开放出来,这样就不用东拼一个libpng,西凑一个libjpeg,最后还来个libwebp。源码兼容Windows/Linux/Android/iOS。(有需要的同学自己到github上load下来)

使用方法也很简单,我们在解压base.apk的assets下的资源文件之后,通过setResPath方法标记 /storage/emulated/0/Android/data/org.zzrblog.nativecpp/cache/assets/mipmap/ 为当前应用的资源文件路径。然后通过拼接完整的资源文件名进行操作。相关代码如下:

    char _res_name[250]={0};
    sprintf(_res_name, "%s%s", res_path, "test.jpg");
    // 输入资源文件的绝对路径,创建纹理
    GLuint _texure_id = TextureHelper::createTextureFromImage(_res_name);
unsigned    createTexture(int w, int h, const void* data, GLenum type)
{
    unsigned    texId;
    glGenTextures(1, &texId);
    glBindTexture(GL_TEXTURE_2D, texId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    glTexImage2D(GL_TEXTURE_2D, 0, type, w, h, 0, type, GL_UNSIGNED_BYTE, data);

    return  texId;
}

GLuint TextureHelper::createTextureFromImage(const char *fileName)
{
    int     width;
    int     height;
    int     chanel;
    GLuint  texId;
    stbi_uc*    pixels  =   stbi_load(fileName, &width, &height, &chanel, 0);
    if (chanel == 3){
        texId   =   createTexture(width, height, pixels, GL_RGB);
    } else {
        texId   =   createTexture(width, height, pixels, GL_RGBA);
    }
    free(pixels);
    return texId;
}

其中stbi_load就是整理封装的各格式图片读取的函数,暂且支持jpg/jpeg,png,bmp,psd四种类别的文件后缀。源码提供在工程目录下的 cpp/common/stb_image.h/cpp,有需要的同学到这里找https://github.com/MrZhaozhirong/NativeCppApp

3、数学矩阵操作(在线找AndroidNative源码)

最后一个问题,其实也不算什么大问题。习惯了系统的android/opengl/Matrix.java提供的各种矩阵方法,到了cpp居然没发现与之相应的矩阵操作库?那么能不能搞一个和Java.Matrix的库一样,使用方式一致的Cpp库?

// Java
Matrix.setIdentityM(modelViewProjectionMatrix,0);
Matrix.setLookAtM(viewMatrix, 0, ... );
Matrix.multiplyMM(viewProjectionMatrix,0,  projectionMatrix,0, viewMatrix,0);
// Cpp
NameSpace::Matrix::setIdentityM(viewMatrix, 0);
NameSpace::Matrix::setLookAtM(viewMatrix, 0, ... );
NameSpace::Matrix::multiplyMM(viewProjectionMatrix,0,  projectionMatrix,0, viewMatrix,0);

这个愿望其实不是什么问题,AndroidSDK的源码是能直接查看到的,我们自己对照着实现就可以了。在愉快的搬砖期间,不愉快的事发送了。

public static native void multiplyMM(float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset);

native方法实现去哪找? 找Android系统源码!去哪找?到 http://androidxref.com/ 找! 就以multiplyMM为例,这里简单介绍一下这个网站的使用方法。

首先进入页面,就显示了各Android系统版本的,我们选择较新的AndroidO-8.1.0。

然后就到搜索页面,Full Search输入你想要搜索的方法的关键字(multiplyMM)左边是要搜索的范围。然后点击search,下方就显示出搜索的结果。

 从搜索的结果我们可以看到,/frameworks/base/opengl/java/android/opengl/Matrix.java 是java层api的入口点,再往下看/frameworks/base/core/jni/android/opengl/util.cpp,这里就是multiplyMM的native方法实现。我们还可以在左侧看到关键字代码的引用,util.cpp的1033行就是静态注册native方法的签名了,事不宜迟,赶紧点进去。(直接点1033行引用就可以了!)

  

到此 Java.Matrix.multiplyMM的native方法实现就找到了!真的非常方便。

通过Androidxref.com的帮助,整理了一套OpenGL常用的数学矩阵静态库,在项目的cpp/common/CELLMath.hpp当中,以后还会不断的扩充。(妈妈再也不怕我不会线性代数啦!)

Note:AndroidStudio 3.x版本CMake的一些问题。

1、More than one file was found with OS independent path 'lib/armeabi-v7a/libxxxxx.so'

解决方法:删除CMakeLists.txt中的 # 设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})

这个问题主要是因为AS3.x的CMake编译器,默认已经是路径了,如果再设置,那么就多生产一份so在指定的目录上,所以就会出现more than one file了

2、Cpp引用第三方so找不到存在文件 

以本篇为例,我们需要使用libzip.so。以前在AS2.x的CMake环境上,我们可以随意设置其so的存放目录
add_library(yuv SHARED IMPORTED )
set_target_properties(yuv PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/libyuv/libyuv.so)
set_target_properties(yuv PROPERTIES LINKER_LANGUAGE CXX)
但是在AS3.x的CMake环境上,我发现不能随意指向存放路径了,问题点还是和上方问题的原因一致,系统只默认so的存放路径,就是项目工程下的 ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libxxx.so,所以我们必须把so存放在这个目录下,并指向这个目录。

3、cpp源文件太多,编译脚本的add_library怎么简化?

之前我们源文件不多的时候,可以这样一一的把源文件全称写上。
add_library( # 生成动态库的名称
             sync-player
             # 指定是动态库SO
             SHARED
             # 编译库的源代码文件
             src/main/cpp/ffmpeg/sync_player.c
             src/main/cpp/ffmpeg/AVPacket_buffer.c
             src/main/cpp/common/zzr_common.c)

现在源文件太多,可以利用aux_source_directory。具体语法如下:
# 将当前 "./src/main/cpp" 等具体目录下的所有源文件保存到 "SELF_DEFINE" 中,然后在 add_library 方法调用。
aux_source_directory(./src/main/cpp/common   COMMON_SRC )
aux_source_directory(./src/main/cpp/egl            EGL_SRC )
aux_source_directory(./src/main/cpp/objects     OBJECTS_SRC )
aux_source_directory(./src/main/cpp/program   PROGRAM_SRC )
aux_source_directory(./src/main/cpp/utils          UTILS_SRC )
aux_source_directory(./src/main/cpp/nativegl    PROJECT_SRC )
add_library(
        native-egl
        SHARED
        ${COMMON_SRC}
        ${EGL_SRC}
        ${OBJECTS_SRC}
        ${PROGRAM_SRC}
        ${UTILS_SRC}
        ${PROJECT_SRC})

最后放一张工程效果照。

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/88880831
今日推荐