【GoogleSamples源码研究】- teapots
本文存在的意义
本文是笔者在研究项目移植过程中的一个产物,目的是将学到的东西记录下来并且分享出去。作为公开发布的文章,本文的侧重点在于分享笔者学到的东西,如果笔者分享的知识有误,请务必指出笔者的错误,让笔者也有一个进步的机会。
在本文中,笔者会根据自己研究项目时学到的东西,从应用启动和操作响应两个角度去分析项目中各个函数的执行过程,目的是理出两条清晰的逻辑线。
警告:本文中含有大量的代码,可能引起胃部不适,请准备好垃圾袋!
准备工作
在开始分析之前,先要弄到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的茶壶,本例中的做法是:
- 创建一个茶壶渲染器,用来进行渲染
- 创建一个Engine用来控制渲染,响应操作
- 创建一个TeapotNativeActivity类,进行UI显示
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结构体
- android_poll_source结构体
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函数中做了以下这些事:
- OpenGL设置
- 加载着色器
- 创建索引缓存
- 创建顶点缓存
- 更新视角
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、操作。
命令
命令是一些系统所提供的消息,例如获取焦点,失去焦点,窗口终止等等.
本项目中,需要将进行操作的主要命令有:
- 保存状态(APP_CMD_SAVE_STATE)
- 初始化窗口(APP_CMD_INIT_WINDOW)
- 终止窗口(APP_CMD_TERM_WINDOW)
- 停止(APP_CMD_STOP)
- 获得焦点(APP_CMD_GAINED_FOCUS)
- 失去焦点(APP_CMD_LOST_FOCUS)
- 内存过低(APP_CMD_LOW_MEMORY)
处理命令的函数是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;
}
}
对不同的操作会产生什么样的命令笔者进行了一些尝试,下面是尝试的结果:
不同操作产生的命令
很明显,不同的操作产生的消息不一样,有时候可能一个操作并没有完全完成就进行了另一个操作,前一个操作产生的命令就不会完整。所以,妥善地处理每一个命令、处理完全是一个好习惯。
输入事件
本例的输入事件一共有三个:
- 1、双击屏幕:这个操作将摄像机摆回原来的位置,与重启APP的效果相同。
- 2、缩放:这个操作将摄像机镜头拉近或者拉远,在界面上显示或大或小的一个茶壶。
- 3、拖动:这个操作旋转摄像机,以看到茶壶的不同地方。
代码以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类