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})
最后放一张工程效果照。