Android NDK程序员

【GoogleSamples源码研究】- teapots

2018-05-10  本文已影响22人  闪电的蓝熊猫

本文存在的意义

本文是笔者在研究项目移植过程中的一个产物,目的是将学到的东西记录下来并且分享出去。作为公开发布的文章,本文的侧重点在于分享笔者学到的东西,如果笔者分享的知识有误,请务必指出笔者的错误,让笔者也有一个进步的机会。

在本文中,笔者会根据自己研究项目时学到的东西,从应用启动和操作响应两个角度去分析项目中各个函数的执行过程,目的是理出两条清晰的逻辑线。

警告:本文中含有大量的代码,可能引起胃部不适,请准备好垃圾袋!

准备工作

在开始分析之前,先要弄到Google Samples的源码。到GitHub上fork一份到自己的repositories中,再download到本地,用Android Studio打开其中的teapots项目,注意,要选择import方式而不是open。两者的区别是:import方式用于导入其他工具(如Eclipse)创建的工程,open方式用于打开以前用Android Studio打开过的工程。笔者用的Android Studio版本是3.0.1,推荐使用3.0之后的版本,新的版本对OpenGL ES支持更好。(当然如果你只是想用来看代码,请无视这段话~)

接下来,我们就顺着两条主要的逻辑线走。

一、启动过程

这是最重要的一条逻辑线,我们要重点关注!

所有的Android APP启动都需要用到Java代码,即便你自己没有写。APP是通过一个叫Zygote的进程fork出来的(类似于Windows上的CreateProcess)。Zygote的中文翻译是受精卵,非常形象地表明所有的子进程都是其“分裂”而来,它是所有进程的“祖先”。Zygote怎么fork出app进程不是我们关注的重点,我们的分析从classic-teapot项目中最先被用到的类开始。

当APP启动时,最初被调用的方法(classic-teapot项目中)是Application类的OnCreate方法。Application类是一个隐藏在背后的类:一方面,每个APP都有这么一个类的实例;另一方面,我们几乎不会实现这个类的函数。它就一直默默地在背后运行。本项目中,OnCreate方法中只有一些输出项目信息的代码,几乎可以忽略不计。

在这个方法之后紧接着会调用的方法就是TeapotNativeActivity类的onCreate函数。由于TeapotNativeActivity类继承自NativeActivity类,teapots项目又是一个native项目,所以它的onCreate就会是继Application.onCreate函数之后第一个调用的函数,这个函数先调用了积累的onCreate,然后对SDK VERSION 19以上的版本进行了隐藏工具条的处理(具体的隐藏方式笔者也没搞懂),启动native线程,进入native代码主函数的操作就隐藏在基类的onCreate函数中,我们直接来看它的代码:

@Override
protected void onCreate (Bundle savedInstanceState) {
        String libname = "main";  // 这是lib名
        String funcname = "ANativeActivity_onCreate";  // 这个函数是要调用的函数
        ActivityInfo ai;

        mIMM = getSystemService(InputMethodManager.class);

        getWindow().takeSurface(this);
        getWindow().takeInputQueue(this);
        getWindow().setFormat(PixelFormat.RGB_565);
        getWindow().setSoftInputMode(
                WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED
                | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

        mNativeContentView = new NativeContentView(this);
        mNativeContentView.mActivity = this;
        setContentView(mNativeContentView);
        mNativeContentView.requestFocus();
        mNativeContentView.getViewTreeObserver().addOnGlobalLayoutListener(this);
        
        try {
            ai = getPackageManager().getActivityInfo(
                    getIntent().getComponent(), PackageManager.GET_META_DATA);  // 在这里会去获取资源(Android Mainifest.xml)文件中定义的数据,打开项目的AndroidManifest.xml文件,可以在其中找到<meta-data android:name="android.app.lib_name"和android:value="TeapotNativeActivity" />两行。这里获取到的meta data就是这个数据块。
            if (ai.metaData != null) {
                String ln = ai.metaData.getString(META_DATA_LIB_NAME);
                if (ln != null) libname = ln;  // lib名已经定义,就是TeapotNativeActivity
                ln = ai.metaData.getString(META_DATA_FUNC_NAME);
                if (ln != null) funcname = ln;  // func名未定义,所以就用初始化时的名字
            }
        } catch (PackageManager.NameNotFoundException e) {
            throw new RuntimeException("Error getting activity info", e);
        }

        BaseDexClassLoader classLoader = (BaseDexClassLoader) getClassLoader();
        String path = classLoader.findLibrary(libname);  // 加载lib

        if (path == null) {
            throw new IllegalArgumentException("Unable to find native library " + libname +
                                               " using classloader: " + classLoader.toString());
        }
        
        byte[] nativeSavedState = savedInstanceState != null
                ? savedInstanceState.getByteArray(KEY_NATIVE_SAVED_STATE) : null;

        // 这里的代码非常关键,加载ANativeActivity_onCreate函数来使用,这个函数我们会深入其实现,看看它到底做了些什么。
        mNativeHandle = loadNativeCode(path, funcname, Looper.myQueue(),
                getAbsolutePath(getFilesDir()), getAbsolutePath(getObbDir()),
                getAbsolutePath(getExternalFilesDir(null)),
                Build.VERSION.SDK_INT, getAssets(), nativeSavedState,
                classLoader, classLoader.getLdLibraryPath());

        if (mNativeHandle == 0) {
            throw new UnsatisfiedLinkError(
                    "Unable to load native library \"" + path + "\": " + getDlError());
        }
        super.onCreate(savedInstanceState);
}

函数主要的功能就是根据配置文件配置lib名和func名,加载native函数来调用。上面的注释中已经详细说明了这个过程,接着我们进入到loadNativeCode函数中。这个函数在android_app_NativeActivity.cpp中,该文件不在我们的项目中,甚至不在NDK bundle中,你需要下载android系统源码才能找到这个文件。在文件中搜索loadNativeCode,首先来到了loadNativeCode_native函数。嗯,名字不一样,应该不是我们想要的,再继续往下搜。直到这个位置:

static const JNINativeMethod g_methods[] = {
  {"loadNativeCode",
    "{Ljava/lang/String;Ljava/lang/String;Landroid/os/MessageQueue;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILandroid/content/res/AssetManager;[BLjava/lang/ClassLoader;Ljava/lang/String;)J",
    (void*)loadNativeCode_native},
...
};

心里咯噔一下,只有这个地方最匹配了,就是这个声明!再仔细一看,这是什么丧尸的声明方法啊,根本不像是c++的语法,倒是和js的语法很像。从声明来看,我们调用的实际函数就是我们第一次定位到的loadNativeCode_native函数,于是我们来到了第二个关键点:

static jlong
loadNativeCode_native(JNIEnv* env, jobject clazz, jstring path, jstring funcName,
                      jobject messageQueue, jstring internalDataDir, jstring obbDir,
                      jstring externalDataDir, jint sdkVersion, jobject jAssetMgr,
                      jbyteArray savedState, jobject classLoader, jstring libraryPath) {
 ...
void* funcPtr = NULL;
const char* funcStr = env ->GetSTringUTFChars(funcName, NULL);  // 将函数名进行一次转换
// 生成函数指针
if (needs_native_bridge) 
  funcPtr = NativeBridgeGetTrampoline(handle, funcStr, NULL, 0);
else
  funcPtr = dlsym(handle, funcStr);

code.reset(new NativeCode(handle, (ANativeActivity_createFunc*)funcPtr));  // 创建NativeCode结构
env ->ReleaseStringUTFChars(funcName, funcStr);
...
code ->createActivityFunc(code.get(), rawSavedState, rawSavedSize); // 调用了创建函数,这个函数到底是什么呢?
...
}

现在,理解的关键变成了new NativeCode的时候对这个函数指针做了什么操作,再看NatvieCode的结构:

struct NativeCode: public ANativeActivity {
  NativeCode(void* _dlhandle, ANativeActivity_createFunc* _createFunc) {
    memset(ANativeActivity*)this, 0, sizeof(ANativeActivity));
    memset(&callbacks, 0, sizeof(callbacks));
    dlhandle = _dlhandle;
    createActivityFunc = _createFunc;
    nativeWindows = NULL;
    mainWorkRead = mainWorkWrite = -1;
  }
};

NativeCode中将ANativeActivity_onCreate函数指针赋值给了createActivityFunc指针,然后在loadNativeCode_native中调用,只不过是换了个马甲,我们照样认得它。

接下来的过程就比较顺利了,ANativeActivity_onCreate函数定义在android_native_app_glue.c文件中,这个文件在我们的项目中就能找到:



打开android_native_app.glue.c文件,定位到ANativeActivity_onCreate函数,找到最关键的一行代码:

JNIEXPORT 
void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState,
                              size_t savedStateSize) {
...
  activity ->instance = android_app_create(activity, savedState, savedStateSize);
}

继续往下找,定位到android_app_create函数:

static struct android_app* android_app_create(ANativeActivity* activity,
    void* savedState, size_t savedStateSize) {
...
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&android_app ->thread, &attr, android_app_entry, android_app);  // 创建线程,调用android_app_entry
...
}

线程创建完成后,会调用android_app_entry函数,这个函数里我们就能找到我们写的android_main函数:

static void* android_app_entry(void* param) {
...
android_main(android_app);  // 这里就是我们可以操作的入口函数了
...
}

终于找到我们项目中的入口函数了。这个入口函数叫的实在是惭愧,前面的东西太多了。

进入到android_main函数,我们终于可以干点正事了。要绘制一个3D的茶壶,本例中的做法是:

android_main函数首先要做的就是初始化Engine,包括:将androd_app指针保存到Engine中、将Engine保存到android_app中、设置android_app中处理APP命令和用户输入的函数、初始化传感器(我们要用来响应横竖两个方向的显示)。完成之后,进入一个无限的显示和读取输入循环,如果有消息就处理,没有消息就绘制界面。

void android_main(android_app* state) {
  LOGI("native-activity: Enter android_main");
  // 初始化引擎
  // 这里需要保存android_app状态,根据android_app的配置信息初始化手势检测,包括doubletap、drag、pinch
  g_engine.SetState(state);
  
  // 初始化辅助函数
  ndk_helper::JNIHelper::Init(state ->activity, HELPER_CLASS_NAME);

  state ->userData = &g_engine;    // 这是我们的app自己的数据
  state ->onAppCmd = Engine::HandleCmd;    // 初始化处理app命令的函数
  state ->onInputEvent = Engine::HandleInput;  // 初始化处理用户输入的函数

  // 这些代码暂时不知道有什么用
#ifdef USE_NDK_PROFILER
  monstartup("libTeapotNativeActivity.so");
#endif

  // 准备监控加速计
  g_engine.InitSensors();

  // 循环等待事件到来
  while (1) {
    // 读取所有的挂起事件
    int id;  // 事件ID
    int events;  // 事件主体
    android_poll_source* source;  // 数据源

    // 如果不在播放动画,我们就一直监听等待事件到来。
    // 如果在播放动画,我们就循环直到所有的事件都被读取,然后绘制下一帧动画
    while ((id = ALooper_pollAll(g_engine.IsReady()? 0 : -1, NULL, &events, (void**)&source)) >= 0) {
      // 处理事件
      if (source != NULL) source ->process(state, source);
      
      g_engine.ProcessSensors(id);

      // 检查一下应用是不是在退出
      if (state ->destroyRequested != 0) {
        g_engine.TermDisplay();
        return;
      }
    }

    if (g_engine.IsReady()) {
      // 绘制被屏幕刷新率所限制,所以这里不需要再计时
      g_engine.DrawFrame();
    }
  }
}

android_main函数本身的代码非常容易理解,值得深究一点的地方下面这几个:

android_app结构体本质上是是一个具有线程的app的胶水代码接口。在这个模型中,app代码运行在独立于进程主线程之外的线程中。该线程不必和Java虚拟机关联,但是如果想要使用JNI来调用Java代码,那就需要关联了。来看源码:

struct android_app {
  // 这是app自己的状态对象
  void* userData;
  
  // 这是处理app命令的函数(命令格式是:APP_CMD_*)
  void (* onAppCmd)(struct android_app* app, int32_t cmd);

  // 这是处理输入事件的函数。到这一步,事件应该已经被预分发了,它将会根据返回值来完成。
  // 如果你处理了这个事件,返回1。如果没有处理,返回0表示接受默认的分发。
  int32_t (* onInputEvent)(struct android_app* app, AInputEvent* event);

  // 这是app运行在这个ANativeActivity对象实例中
  ANativeActivity* activity;
  
  // app运行在这个配置中
  AConfiguration* config;

  // 这是实例最后保存的状态,就像在创造的时候提供的那样。如果没有状态,这会是一个NULL值。
  // 你可以根据自己的需要使用它。这块内存会保留着,这道你调用了android_app_exec_cmd()来处理APP_CMD_RESUME命令。如果你执行了这个操作,savedState指向的内存区域就会被释放,它的值也会被设置成NULL。
  // 这些变量应该只在处理APP_CMD_SAVE_STATE命令的时候改变,这时,savedState会被设置成NULL,你也可以重新分配内存保存信息了。
  // 这种情况下,内存会在之后自动替你释放掉。
  void* savedState;
  size_t savedStateSize;

  //这是关联到app线程上的
  ALooper* looper;
  
  // 如果不为空,这就是接受用户输入事件的队列
  AInputQueue* inputQueue;

  // 如果不为空,这就是app可以绘制的窗口
  ANativeWindow* window;

  // 当前窗口的内容区域。这个区域是用户可以看到的地方。
  ARect contentRect;

  // app活动的状态。可以是APP_CMD_START, APP_CMD_RESUME, APP_CMD_PAUSE,或者APP_CMD_STOP。
  int activityState;

  // 这个值会被设置成非0值,当app的NativeActivity正在被销毁,并且等待app线程完成其工作的时候。
  int destroyRequested;

  // 下面的都是“私有”成员
  // (这些不是靠语法约束,而是靠人为约束的东西)
  pthread_mutex_t mutex;
  pthread_cond_t cond;

  int msgread;
  int msgwrite;

  pthread_t thread;
  
  struct android_poll_source cmdPollSource;
  struct android_poll_source inputPollSource;
  
  int running;
  int stateSaved;
  int destroyed;
  int redrawNeeded;
  AInputQueue* pendingInputQueue;
  ANativeWindow* pendingWindow;
  ARect pendingContentRect;
};

android_poll_source是一个和ALooper关联的数据结构。如果源的数据已经准备完成,它会返回一个数据。

struct android_poll_source {
  // 这个源的标识符。可以是LOOPER_ID_MAIN或者LOOPER_ID_INPUT
  int32_t id;

  // 相关联的android_app对象
  struct android_app* app;

  // 用来处理数据的函数
  void (* process)(struct android_app* app, struct android_poll_source* source);
};

其他的代码都很容易理解,最复杂的类就是项目中自定义的Engine类。Engine类表示的是一种管理员角色的抽象,它负责对整个显示进行一个控制,什么时候该显示什么东西,输入事件应该由谁去处理,它都需要清楚并且能准确地将事情分派给内部的对象去处理:

class Engine {
  TeapotRenderer renderer_;    // 这是我们自己定义的渲染器
  ndk_helper::GLContext* gl_context_;    // OpenGL和EGL上下文处理器
  
  bool initialized_resources;  // 标志是否初始化了资源
  bool has_focus_;  // 是否有焦点

  // 下面是一些监控器,包括双击事件、缩放(pinch)事件、拖拽(drag)事件,以及性能的监控(最后一个monitor)
  ndk_helper::DoubletapDetector doubletap_detector_;
  ndk_helper::PinchDetector pinch_detector_;
  ndk_helper::DragDetector drag_detector_;
  ndk_helper::PerfMonitor monitor_;

  ndk_helper::TapCamera tap_camera_;  // 摄像机对象

  android_app* app_;    // android app对象

  ASensorManager* sensor_manager_;    // 传感器管理器
  const ASendor* accelerometer_sensor_;  // 加速计传感器
  ASensorEventQueue* sensor_event_queue_;  // 传感器事件队列

  /* 接下来就是函数了 */
  void UpdateFPS(float fFPS);  // 更新FPS信息的函数
  void ShowUI();    // 显示UI的函数
  void TransformPosition(ndk_helper::Vec2& vec);  // 这个函数用于转换操作的位置信息,将其转换成摄像机可以使用的向量
  /* 以上这些都是私有成员 */

public:
  static void HandleCmd(struct android_app* app, int32_t cmd);  // 处理主要命令的函数
  static int32_t HandleInput(android_app* app, AInputEvent* event);  // 处理输入事件的函数
  Engine();
  ~Engine();
  void SetState(android_app* app);    // 设置android app的状态
  int InitDisplay(andoird_app* app);  // 初始化显示
  void LoadResources();  // 加载资源:对渲染器初始化,将摄像机绑定到渲染器上
  void UnloadResources();  // 卸载资源:将渲染器卸载
  void DrawFrame();    // 绘制一帧
  void TermDisplay();  // 卸下与显示器关联的EGL上下文
  void TrimMemory();  // 减少内存
  bool IsReady();  // 是否获得了焦点(表示已经准备好显示了)

  void UpdatePosition(AInputEvent* event, int32_t iIndex, float& fX, float& fY);  // 这个函数没有用
  
  // 下面这些函数只有一个架子,捕获了事件之后没有进行处理
  void InitSensors();  // 初始化传感器,将sensor_manager_,accelerometer_sensor_和sensor_event_queue_初始化
  void ProcessSensors(int32_t id);  // 处理传感器数据
  void SuspendSensors();  // 挂起(停用)传感器
  void ResumeSensors();  // 重启传感器
}

值得注意的一点是:Engine类中UpdateFPS和ShowUI两个函数需要调用TeapotNativeActivity类中同名的方法,在c++代码中调用Java代码的其中一种方法就是像这两个函数一样,我们看其中一个函数就可以了,比如UpdateFPS函数:

void Engine::UpdateFPS(float fFPS) {
  JNIEnv* jni;
  app_ ->activity ->vm ->AttachCurrentThread(&jni, NULL);  // 通过附加到当前线程获取JNI环境信息

  // 获取Java类
  jclass clazz = jni ->GetObjectClass(app_ ->activity ->clazz);
  // 获取updateFPS函数,最后的字符串是参数与返回值,括号里的是参数,F表示float,后面的是返回值,V表示void
  jmethodID methodID  = jni ->GetMethodID(clazz, "updateFPS", "(F)V");
  // 调用方法
  jni ->CallVoidMethod(app_ ->app ->clazz, methodID, fFPS);
  
  // 最后将线程卸下
  app_ ->activity ->vm ->DetachCurrentThread();
}

调用Java代码的方法很简单,获取Java类,获取方法ID,调用call函数就可以用了。最后我们再来看一个绘制一帧的函数,DrawFrame。在Engine类中,这是最后一个我们感兴趣的函数:

void Engine::DrawFrame() {
  float fps;
  if (monitor_.Update(fps)) {
    UpdateFPS(fps);
  }
  renderer_.Update(monitor_.GetCurrentTime());

  // 这是OpenGL中的绘制流程
  glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  renderer_.Render();

  // 交换缓冲区
  if (EGL_SUCCESS != gl_context_ ->Swap()) {
    // 如果交换缓冲区失败,卸载渲染器后再加载一次
    UnloadResources();
    LoadResources();
  }
}

函数的实现很熟悉,与OpenGL中的绘制类似。首先需要清空一下屏幕,然后调用渲染器的Render函数,在绘制到离屏缓存后,交换一下缓存。

了解完“管理员”之后,我们就要来了解一下“操作工”了。本例中,渲染操作工的角色由TeapotRenderer类来承担。它要做的工作就是将茶壶模型从文件中加载进程序,然后显示到界面上。操作的过程和单独使用OpenGL绘制模型类似,有兴趣的读者可以参考笔者之前写过的OpenGL学习文章。

在OpenGL中绘制模型有一些很格式化的流程,比如你需要创建一个顶点缓存,一个索引缓存,或许还要一个帧缓存。本例中的流程是:1、加载模型文件。2、创建索引缓存。3、创建顶点缓存。4、加载着色器。5、摄像机变换。6、渲染。流程十分清晰,先来看看TeapotRenderer类里的东东:

// 顶点结构
struct TEAPOT_VERTEX {
  float pos[3];      // 顶点位置
  float normal[3];  // 顶点法向量
};

// 着色器属性枚举
enum SHADER_ATTRIBUTES {
  ATTRIB_VERTEX,    // 属性:顶点
  ATTRIB_NORMAL,  // 属性:法向量
  ATTRIB_UV,  // 属性:UV
};

// 着色器结构
struct SHADER_PARAMS {
  GLuint program_;  // 着色程序
  GLuint light0_;  // 光照
  GLuint material_diffuse_;  // 材质:漫反射
  GLuint material_ambient_;  // 材质:环境光
  GLuint material_specular_;  // 材质:镜面高光
  
  GLuint matrix_projection_;  // 矩阵:投影
  GLuint matrix_view_;  // 矩阵:观察
};

// 茶壶的材质结构
struct TEAPOT_MATERIALS {
  float diffuse_color[3];    // 漫反射颜色
  float specular_color[4];  // 镜面高光颜色
  float ambient_color[3];  // 环境光颜色
};

// 茶壶渲染器
class TeapotRenderer {
  int32_t num_indices_;    // 索引数量
  int32_t num_vertices_;  // 顶点数量
  GLuint ibo_;    // index buffer object
  GLuint vbo_;   // vertex buffer object
  
  SHADER_PARAMS shader_param_;  // 着色器程序
  bool LoadShaders(SHADER_PARAMS* params, const char* strVsh, const char* strFsh);

  ndk_helper::Mat4 mat_projection_;  // 投影矩阵
  ndk_helper::Mat4 mat_view_;  // 观察矩阵
  ndk_helper::Mat4 mat_model_;  // 模型矩阵

  ndk_helper::TapCamera* camera_;  // 摄像机

public:
  // 构造&析构函数
  ...
  void Init();  // 初始化
  void Render();  // 渲染
  void Update(float dTime);  // 刷新
  bool Bind(ndk_helper::TapCamera* camera);  // 绑定摄像机,就是把camera赋值给camera_而已
  void Unload();  // 卸载
  void UpdateViewport();  // 更新视角
};

这里我把需要用到的数据结构也贴出来并且加了注释,有过OpenGL编程经验的人应该会感到很熟悉,没有的人也没关系,不妨碍理解。代码非常清晰,函数基本上也做到了自解释的目的,初始化,渲染,更新等等操作一目了然。

先看初始化函数(Init)。Init函数中做了以下这些事:

void TeapotRenderer::Init() {
  // 设置顶点绕序
  glFrontFace(GL_CCW);

  // 加载着色器
  LoadShaders(&shader_param_, "Shaders/VS_ShaderPlain.vsh",
              "Shaders/ShaderPlain.fsh");

  // 创建索引缓存
  num_indices_ = sizeof(teapotIndices) / sizeof(teapotIndices[0]);
  glGenBuffers(1, &ibo_);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(teapotIndices), teapotIndices,
               GL_STATIC_DRAW);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

  // 创建顶点缓存
  num_vertices_ = sizeof(teapotPositions) / sizeof(teapotPositions[0]) / 3;
  int32_t stride = sizeof(TEAPOT_VERTEX);
  int32_t index = 0;
  TEAPOT_VERTEX* p = new TEAPOT_VERTEX[num_vertices_];
  for (int32_t i = 0; i < num_vertices_; ++i) {
    p[i].pos[0] = teapotPositions[index];
    p[i].pos[1] = teapotPositions[index + 1];
    p[i].pos[2] = teapotPositions[index + 2];

    p[i].normal[0] = teapotNormals[index];
    p[i].normal[1] = teapotNormals[index + 1];
    p[i].normal[2] = teapotNormals[index + 2];
    index += 3;
  }
  glGenBuffers(1, &vbo_);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_);
  glBufferData(GL_ARRAY_BUFFER, stride * num_vertices_, p, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  delete[] p;

  // 更新视角
  UpdateViewport();
  mat_model_ = ndk_helper::Mat4::Translation(0, 0, -15.f);

  ndk_helper::Mat4 mat = ndk_helper::Mat4::RotationX(M_PI / 3);
  mat_model_ = mat * mat_model_;
}

里面调用的UpdateViewport()函数的作用是根据屏幕的横竖来生成投影矩阵并保存,具体如下:

void TeapotRenderer::UpdateViewport() {
  // 初始化投影矩阵
  int32_t viewport[4];
  glGetIntegerv(GL_VIEWPORT, viewport);  // 这个函数会返回视口的x和y坐标(窗口坐标),然后返回视口的宽度和高度

  const float CAM_NEAR = 5.0f;
  const float CAM_FAR = 10000.f;
  if (viewport[2] < viewport[3]) {
    float aspect = static_cast<float>(viewport[2]) / static_cast(float)(viewport[3]);
    mat_projection_ = ndk_helper::Mat4::Perspective(aspect, 1.0f, CAM_NEAR, CAM_FAR);
  }
  else {
    float aspect = static_cast<float>(viewport[3]) / static_cast<float> (viewport[2]);
    mat_projection_ = ndk_helper::Mat4::Perspective(1.f, aspect, CAM_NEAR, CAM_FAR);
  }
}

加载着色器(LoadShaders)方法中,不仅需要将着色器编译链接,还需要将着色器的一些uniform变量位置获取到,保存起来,来看:

bool TeapotRenderer::LoadShaders(SHADER_PARAMS* params, const char* strVsh, const char* strFsh) {
  GLuint program;
  GLuint vert_shader, frag_shader;
  
  // 创建着色器程序
  program = glCreateProgram();
  LOGI("Created Shader %d", program);
  
  // 创建以及编译顶点着色器
  if (!ndk_helper::shader::CompileShader(&vert_shader, GL_VERTEX_SHADER, strVsh)) {
    LOGI("Failed to compile vertex shader");
    glDeleteProgram (program);
    return false;
  }

  // 创建以及编译片元着色器
  if (!ndk_helper::shader::CompileShader (&frag_shader, GL_FRAGMENT_SHADER, strFsh)) {
    LOGI ("Failed to compile vertex shader");
    glDeleteProgram (program);
    return false;
  }

  // 将顶点着色器和片元着色器附加到着色器程序上
  glAttachShader (program, vert_shader);
  glAttachShader (program, frag_shader);

  // 绑定属性位置,这个操作需要在链接之前完成
  glBindAttribLocation (program, ATTRIB_VERTEX, "myVertex");
  glBindAttribLocation (program, ATTRIB_NORMAL, "myNormal");
  glBindAttribLocation (program, ATTRIB_UV, "myUV");

  // 链接着色器程序
  if (!ndk_helper::shader::LinkProgram (program)) {
    if (vert_shader) {
      glDeleteShader (vert_shader);
      vert_shader = 0;
    }
    if (frag_shader) {
      glDeleteShader (frag_shader);
      frag_shader = 0;
    }
    if (program) {
      glDeleteProgram (program);
    }
    return false;
  }
  
  // 定位uniform变量位置
  params ->matrix_projection_ = glGetUniformLocation (program, "uPMatrix");
  params ->matrix_view_ = glGetUnifromLocation (program, "uMVMatrix");
  params ->light0_ = glGetUnifromLocation (program, "vLight0");
  params ->material_diffuse_ = glGetUniformLocation (program, "vMaterialDiffuse");
  params ->material_specular_  = glGetUniformLocation (program, "vMaterialSpecular");
  
  if (vert_shader) glDeleteShader (vert_shader);
  if (frag_shader) glDeleteShader (frag_shader);

  params ->program_ = program;
  return true;
}

Init函数涉及到的东西大概就是这些,Update函数主要是更新观察矩阵用的:

void TeapotRenderer::Update (double time) {
  const float CAM_X = 0.f;
  const float CAM_Y = 0.f;
  const float CAM_Z = 0.f;
  
  // 摄像机位置是(CAM_X, CAM_Y, CAM_Z)
  // 观察的目标位置是(0.f, 0.f, 0.f)
  // 上方向量是(0.f, 1.f, 0.f)
  mat_view_ = ndk_helper::Mat4::LookAt (ndk_helper::Vec3 (CAM_X, CAM_Y, CAM_Z),
      ndk_helper::Vec3 (0.f, 0.f, 0.f),
      ndk_helper::Vec3 (0.f, 1.f, 0.f));

  // 更新,并且把观察矩阵保存下来
  if (camera_) {
    camera_ ->Update (time);
    mat_view_ = camera_ ->GetTransformMatrix() * mat_view_ * camera_ -> GetRotationMatrix() * mat_model_;
  }
  else {
    mat_view_ = mat_view_ * mat_model_;
  }
}

最后是Render函数,从名字就知道这是用来绘制,我们来看具体的实现:

void TeapotRenderer::Render (float r, float g, float b) {
  // 观察矩阵+透视矩阵
  ndk_helper::Mat4 mat_vp = mat_projection_ * mat_view_;
  
  // 绑定VBO
  glBindBuffer (GL_ARRAY_BUFFER, vbo_);

  int32_t iStride = sizeof (TEAPOT_VERTEX);
  // 设置顶点属性
  glVertexAttribPointer (ATTRIB_VERTEX, 3, GL_FLOAT, GL_FALSE, iStride, BUFFER_OFFSET (0));
  glEnableVertexAttribArray (ATTRIB_VERTEX);
  
  glVertexAttribPointer (ATTRIB_NORMAL, 3, GL_FLOAT, GL_FLASE, iStride, BUFFER_OFFSET (3 * sizeof (GLfloat)));
  glEnableVertexAttribArray (ATTRIB_NORMAL);

  // 绑定索引缓存
  glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, ibo_);

  // 着色器程序启动
  glUseProgram (shader_param_.program_);

  // 材质
  TEAPOT_MATERIALS material = { {r, g, b}, {1.0f, 1.0f, 1.0f, 10.0f}, {0.1f, 0.1f, 0.1f}};
  
  // 更新uniform变量
  glUniform4f (shader_param_.material_diffuse_, material.diffuse_color[0],
    material.diffuse_color[1], material.diffuse_color[2], 1.0f);
  glUniform4f (shader_param_.material_specular_, material.specular_color[0],
    material.specluar_color[1], material.specular_color[3]);
  
  glUniform3f (shader_param_.material_ambient_, material.ambient_color[0], material.ambient_color[1], material.ambient_color[2]);

  glUniformMatrix4fv (shader_param_.matrix_projection_, 1, GL_FALSE, mat_vp.Ptr());
  glUniformMatrix4fv (shader_param_.matrix_view_, 1, GL_FALSE, mat_view_.Ptr());
  glUniform3f (shader_param_.light0_, 100.f, -200.f, -600.f);

  // 绘制
  glDrawElements (GL_TRIANGLES, num_indices_, GL_UNSIGNED_SHORT, BUFFER_OFFSET(0));
  
  // 重置ABO和EBO
  glBindBuffer (GL_ARRAY_BUFFER, 0);
  glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, 0);
}

如同代码中注释的那样,绘制需要先设置属性,包括顶点属性、VBO、VIO以及uniform变量。然后使用glDrawElements绘制,在glDrawElement绘制过程中,OpenGL会调用shader程序对顶点进行处理,绘制出我们想要的图形。

至此,启动并且显示的流程已经分析完毕,松了一大口气。接下来就是操作响应!

二、操作响应

APP中需要的响应的操作有两种:1、命令。2、操作。

命令

命令是一些系统所提供的消息,例如获取焦点,失去焦点,窗口终止等等.

本项目中,需要将进行操作的主要命令有:

处理命令的函数是HandleCmd函数:

/**
 * 处理主要命令
 */
void Engine::HandleCmd(struct android_app* app, int32_t cmd) {
  Engine* eng = (Engine*) app ->userData;
  switch (cmd) {
    case APP_CMD_SAVE_STATE:
      break;
    case APP_CMD_INIT_WINDOW:  
      // 初始化窗口,完成准备工作
      if (app ->window != NULL) {
        eng ->InitDisplay(app);
        eng ->DrawFrame();
      }
      break;
    case APP_CMD_TERM_WINDOW:
      // 窗口就要被隐藏或者关闭了,执行清理工作
      eng ->TermDisplay();
      eng ->has_focus_ = false;
      break;
    case APP_CMD_STOP:
      break;
    case APP_CMD_GAINED_FOCUS:
      eng ->ResumeSensors();
      // 开始动画
      eng ->has_focus_ = true;
      break;
    case APP_CMD_LOST_FOCUS:
      eng ->SuspendSensors();
      // 停止动画
      eng ->has_focus_ = false;
      eng ->DrawFrame();
      break;
    case APP_CMD_LOW_MEMORY:  
      // 释放GL资源
      eng ->TrimMemory();
      break;
  }
}

对不同的操作会产生什么样的命令笔者进行了一些尝试,下面是尝试的结果:


不同操作产生的命令

很明显,不同的操作产生的消息不一样,有时候可能一个操作并没有完全完成就进行了另一个操作,前一个操作产生的命令就不会完整。所以,妥善地处理每一个命令、处理完全是一个好习惯。

输入事件

本例的输入事件一共有三个:

代码以if...else...结构作为区分,调用Engine的三个操作检测器判断发生的操作类型,然后进行相应的处理:

int32_t Engine::HandleInput(android_app* app, AInputEvent* event) {
  Engine* eng = (Engine*)app ->userData;
  // 事件类型有两种,一种是按键事件(AINPUT_EVNET_TYPE_KEY),一种是触摸事件(AINPUT_EVENT_TYPE_MOTION)
  // 我们队按键事件不感兴趣,因此只针对触摸事件进行处理
  if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) 
      // 用三个检测器对事件进行检测
      ndk_helper::GESTURE_STATE doubleTapState = eng ->doubletap_detector_.Detect(event);
      ndk_helper::GESTURE_STATE dragState = eng ->drag_detector_.Detect(event);
      ndk_helper::GESTRUE_STATE pinchState = eng ->pinch_detector_.Detect(event);

      // 双击事件的优先级最高
      if (doubleTapState == ndk_helper::GESTURE_STATE_ACTION) {
        // 重置摄像机位置
        eng ->tap_camera_.Reset(true);
      }
      else {
        // 处理拖动事件
        if (dragState & ndk_helper::GESTURE_STATE_START) {
          // 开始拖动
          ndk_helper::Vec2 v;
          eng ->drag_detector_.GetPointer(v);
          eng ->TransformPosition(v);
          eng ->tap_camera_.Drag(v);
        }
        else if (dragState & ndk_helper::GESTURE_STATE_MOVE) {
          ndk_helper::Vec2 v;
          eng ->drag_detector_.GetPointer(v);
          eng ->TransformPosition(v);
          eng ->tap_camera_.Drag(v);
        }
        else if (dragState & ndk_helper::GESTURE_STATE_END) 
          eng ->tap_camera_.EndDrag();

       // 处理pinch操作
       if (pinchState & ndk_helper::GESTURE_STATE_START) {
          // 开始新pinch操作
          ndk_helper::Vec2 v1;
          ndk_helper::Vec2 v2;
          eng ->pinch_detector_.GetPointers(v1, v2);
          eng ->TransformPosition(v1);
          eng ->TransformPosition(v2);
          eng ->tap_camera_.BeginPinch(v1, v2);
       }
       else if (pinchState & ndk_helper::GESTURE_STATE_MOVE) {
          // 多点触碰
          // 开始新pinch
          ndk_helper::Vec2 v1;
          ndk_helper::Vec2 v2;
          eng ->pinch_detector_.GetPointers(v1, v2);
          eng ->TransformPosition(v1);
          eng ->TransformPosition(v2);
          eng ->tap_camera_.Pinch(v1, v2);
       }
      }
      return 1;
  }
  return 0;
}

到这里,两条线都已经梳理完毕。

总结

我们来复习一下,启动线是从Application类开始,经过TeapotNativeActivity类,到android_app_NativeActivity.cpp中的loadNativeCode_native函数,再到android_native_app.glue.c中的ANativeActivity_onCreate函数,最后到android_main函数启动。启动之后就是无限的等待事件循环。操作线是从我们(使用者)的操作开始,进入相应的处理函数(命令或者操作),逐个处理操作。整理一遍之后,逻辑就非常清晰了。

参考资料

Android:全面解析 熟悉而陌生 的Application类使用:详细了解Application类

上一篇下一篇

猜你喜欢

热点阅读