春节临近本来不想更了,但是为了纪念即将逝去的一年,还是留下点什么吧!就让我们用OpenglES实现一个纯native的png图片的贴图!
我采用的是Android Studio!Android提供了NativeActivity
来实现纯Native应用,我们将Native的实现打包成一个共享库,通过NativeActivity
来调对应的共享库。创建一个native的应用,和正常项目大同小异,总的来说,主要注意一下几个部分:
时选择 Native C++,C++ 标准我采用的是C++ 14
添加Activity,名称就是android.app.NativeActivity
,主要的就是这里的meta-data
,native打包成共享库opengles_simples
。
在一个项目中,java代码和Native代码是可以共存的,如果没有java代码,将android:hasCode
设置为false。
Android Studio中,采用CMake来编译,所以我们需要准备好CMakeLists.txt
和对应的native代码!
将native代码Link到Studio的项目
右击项目名,会出现Link C++ Project with Gradle
,点击进去找到我们的CMakeLists.txt
文件添加就行!
Native实现
NativeActivity
的Native实现基于native_app_glue
,native_app_glue
以静态库的方式提供。Native代码实现,需要实现native_app_glue
的接口android_main
。
#include
void android_main(struct android_app *app) {
//Empty
}
cmake_minimum_required(VERSION 3.4.1)
# add static lib native_activity_glue
add_library(native_activity_glue STATIC
${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c)
# solve compile error
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -Werror -D VK_USE_PLATFORM_ANDROID_KHR")
# solve compile error
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
# add native shared lib
add_library(opengles_simples
SHARED
NativeMain.cpp)
target_include_directories(opengles_simples PRIVATE
${ANDROID_NDK}/sources/android/native_app_glue)
target_link_libraries(opengles_simples
native_activity_glue
android
log)
首先添加NativeActivity
的native实现,静态库native_activity_glue
,代码在ndk目录下。再添加native的实现,编译为共享库,对应的文件NativeMain.cpp
,这里是相对CMakeList.txt
的路径。target_link_libraries
将需要的库连接到对应的native共享库。
OK,运行一下,是不是一个纯背景的界面就起来了!注意,这里的android_main
是native的入口,如果没有是会报错的!
Java层实现
frameworks/base/core/java/android/app/NativeActivity.java
JNI实现
frameworks/base/core/jni/android_app_NativeActivity.cpp
NDK实现
ndk-bundle/sources/android/native_app_glue
NativeActivity
继承Activity,在onCreate
时,将加载对应的so库,找到并调用native的创建方法ANativeActivity_onCreate
。在ANativeActivity_onCreate
中将注册Activity相关callback,创建对应的android_appandroid_app_create
。
JNIEXPORT
void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState,
size_t savedStateSize) {
LOGV("Creating: %p\n", activity);
activity->callbacks->onDestroy = onDestroy;
activity->callbacks->onStart = onStart;
activity->callbacks->onResume = onResume;
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
activity->callbacks->onPause = onPause;
activity->callbacks->onStop = onStop;
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
activity->callbacks->onLowMemory = onLowMemory;
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
activity->callbacks->onInputQueueCreated = onInputQueueCreated;
activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
activity->instance = android_app_create(activity, savedState, savedStateSize);
}
创建android_app时,在android_app_entry
中,将调我们自己的实现android_main
。
/**
* This is the function that application code must implement, representing
* the main entry to the app.
*/
extern void android_main(struct android_app* app);
Activity的对应的生命周期等,都通过callbacks调到native。我们也可以去监听对应的状态变化。
实现native应用时,我们主要实现下面几个主要的功能:
struct android_app {
// The application can place a pointer to its own state object
// here if it likes.
void* userData;
// Fill this in with the function to process main app commands (APP_CMD_*)
void (*onAppCmd)(struct android_app* app, int32_t cmd);
// Fill this in with the function to process input events. At this point
// the event has already been pre-dispatched, and it will be finished upon
// return. Return 1 if you have handled the event, 0 for any default
// dispatching.
int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event);
enum {
APP_CMD_INPUT_CHANGED,
APP_CMD_INIT_WINDOW,
APP_CMD_TERM_WINDOW,
APP_CMD_WINDOW_RESIZED,
APP_CMD_WINDOW_REDRAW_NEEDED,
APP_CMD_CONTENT_RECT_CHANGED,
APP_CMD_GAINED_FOCUS,
APP_CMD_LOST_FOCUS,
APP_CMD_CONFIG_CHANGED,
APP_CMD_LOW_MEMORY,
APP_CMD_START,
APP_CMD_RESUME,
APP_CMD_SAVE_STATE,
APP_CMD_PAUSE,
APP_CMD_STOP,
APP_CMD_DESTROY,
};
enum {
/** Indicates that the input event is a key event. */
AINPUT_EVENT_TYPE_KEY = 1,
/** Indicates that the input event is a motion event. */
AINPUT_EVENT_TYPE_MOTION = 2
};
具体的实现去看代码吧!
除这些事件外,还可以获取到Sensor的数据ASensorEventQueue_getEvents
,能够满足所有开发的需求!!!
Native中没有现成的图片加载(编解码),但是我们可以用libpng等库来满足我们的需求!png相关的内容,前面我已经有文章整理过,可以参考 PNG编解码
前面的Demo代码中,我是重新的的libpng的CMakeList.txt,其实libpng的源码中已经有CMakeList.txt,我们直接include进来就行了。注意,png基于zlib,所以,我们也要把zlib也include进来。
# 3.build zlib
get_filename_component(GLMINC_PREFIX
"${CMAKE_SOURCE_DIR}/zlib/zlib-1.2.11"
ABSOLUTE)
add_subdirectory(${CMAKE_SOURCE_DIR}/zlib/zlib-1.2.11 ${CMAKE_BINARY_DIR}/zlib)
# 4.build -png
set(PNG_STATIC ON)
set(PNG_SHARED OFF)
set(PNG_TESTS OFF)
get_filename_component(GLMINC_PREFIX
"${CMAKE_SOURCE_DIR}/png/libpng-1.6.37"
ABSOLUTE)
add_subdirectory(${CMAKE_SOURCE_DIR}/png/libpng-1.6.37 ${CMAKE_BINARY_DIR}/png)
编译png库时,PNG_STATIC
的这几个宏需要设置,我们只要静态库就行了。连接的时候,连接对应的静态库:
target_link_libraries( opengles_simples
... ...
zlibstatic
png_static
)
采用的结构和函数如下,具体的实现看代码吧!
png_create_read_struct
png_create_info_struct
png_init_io
png_read_png
png_get_rows
png_destroy_read_struct
png解码出来的数据根据png_get_color_type获取对应的颜色类型,我们采用应该是RGB或RGBA。
我们需要将PNG图片进行贴图,我们采用一个结构来描述解码后的PNG图片吧!
struct gl_texture_t {
GLsizei width;
GLsizei height;
GLenum format;
GLint internalFormat;
GLuint id;
GLubyte *pixels;
};
解码后的数据就放在了pixels
中。
OpenGL不多赘述,我们可以通过图片贴图来让OpenGL变的斑斓灿烂。我们就来看看怎么实现贴图吧!
GLuint CreateTexture2D(UserData *userData, char* fileName) {
// Texture object handle
GLuint textureId;
userData->texture = readPngFile(fileName);
// Generate a texture object
glGenTextures(1, &textureId);
// Bind the texture object
glBindTexture(GL_TEXTURE_2D, textureId);
// Load mipmap level 0
glTexImage2D(GL_TEXTURE_2D, 0, userData->texture->format, userData->texture->width,
userData->texture->height,
0, userData->texture->format, GL_UNSIGNED_BYTE, userData->texture->pixels);
// Set the filtering mode
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
free(userData->texture->pixels);
free(userData->texture);
return textureId;
}
glGenTextures
创建一个纹理,相当于我们平常编程时malloc一块内存一样,这里是在glBindTexture
是将纹理进行绑定,将申请的Texture和一个2D的纹理进行绑定;glTexImage2D
将图片数据加载到纹理中,包括数据的描述和数据pixels
。GL_NEAREST
和GL_LINEAR
,前者表示“使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色”,后者表示“使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色”。通过下图很好的解释这两者差别:创建好的Texture,是没有激活的,我们是根据textureId来获取纹理。启用纹理映射后,如果想把一幅纹理映射到相应的几何图元,就必须告诉GPU如何进行纹理映射,也就是为图元的顶点指定恰当的纹理坐标。
纹理的映射需要将纹理的坐标和屏幕的坐标进行映射。这就需要理解纹理坐标系和屏幕坐标系,纹理坐标的远点在坐下角,而屏幕坐标的远点在左上角。
GLfloat vVertices[] = {-0.3f, 0.3f, 0.0f, 1.0f, // Position 0
-1.0f, -1.0f, // TexCoord 0
-0.3f, -0.3f, 0.0f, 1.0f, // Position 1
-1.0f, 2.0f, // TexCoord 1
0.3f, -0.3f, 0.0f, 1.0f, // Position 2
2.0f, 2.0f, // TexCoord 2
0.3f, 0.3f, 0.0f, 1.0f, // Position 3
2.0f, -1.0f // TexCoord 3
};
GLushort indices[] = {0, 1, 2, 0, 2, 3};
这是vVertices
这是 混合写法,将纹理的坐标和屏幕的坐标一一对应,是不是很不好理解,我们来个图,一个就明白了!
两个坐标系的方向不一样,所以vVertices
看起来怪怪的,要加以区分!
indices
是绘制的顺序,OpenGLES最复杂只能按照三角形进行渲染,所以这里的索引就是告诉GPU,前面定义的vVertices
中顶点,怎么组成三角形。indices
中定义的索引将按照下图的方式进行渲染。
// Load the vertex position
glVertexAttribPointer(0, 4, GL_FLOAT,
GL_FALSE, 6 * sizeof(GLfloat), vVertices);
// Load the texture coordinate
glVertexAttribPointer(1, 2, GL_FLOAT,
GL_FALSE, 6 * sizeof(GLfloat), &vVertices[4]);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
这里的0
,1
是前面加载坐标是对应的索引,和Shader中的坐标索引也是一一对应的!
// Bind the texture
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, userData->textureId);
// Set the sampler texture unit to 0
glUniform1i(userData->samplerLoc, 0);
激活的是GL_TEXTURE0,GL_TEXTURE0和前面已经加载好的纹理绑定!glUniform1i(userData->samplerLoc, 0)
是将GL_TEXTURE0和Shader中的纹理进s_texture
行绑定!这样Shader中的纹理s_texture
就和我们前面已经加载好的纹理进行绑定了。
纹理包装模式 | 描述 |
---|---|
GL_REPEAT | 重复纹理 |
GL_CLAMP_TO_EDGE | 限定读取纹理的边缘,边缘延伸 |
GL_MIRRORED_REPEAT | 重复纹理和镜像纹理 |
代码实现:
// Draw quad with repeat wrap mode
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glUniform1f(userData->offsetLoc, -0.7f);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
// Draw quad with clamp to edge wrap mode
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glUniform1f(userData->offsetLoc, 0.0f);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
// Draw quad with mirrored repeat
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
glUniform1f(userData->offsetLoc, 0.7f);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
我们的示例,是通过Shader来进行纹理的贴图的!这是我们采用的顶点Shader和片Shader。
char vShaderStr[] =
... ...
"void main() \n"
"{ \n"
" gl_Position = a_position; \n"
" v_texCoord = a_texCoord; \n"
"} \n";
char fShaderStr[] =
... ...
"uniform sampler2D s_texture; \n"
"void main() \n"
"{ \n"
" outColor = texture( s_texture, v_texCoord ); \n"
"} \n";
通过outColor = texture( s_texture, v_texCoord );
得到我们需要的颜色值!
Shader加载:
// Load the shaders and get a linked program object
userData->programObject = loadProgram(vShaderStr, fShaderStr);
program的使用:
// Use the program object
glUseProgram(userData->programObject);
Android的权限管理越来越严格和精细化,好久没有写应用,发现SD的读写权限申请也很麻烦,看了半天源码,幸好还有补救的办法!这里记下以备后续之需!
之前老是想从native直接申请权限,后来发现行不通,所以转而回到Java层!我们增加一个启动界面StartingActivity
,在StartingActivity
中申请权限,获取到权限后立即启动NativeActivity
,关掉StartingActivity
,这样问题就可以完美解决!
总的说来,申请权限需要注意以下几点:
之前是不用申明的,是因为在新的Android版本,又有新的SD卡读写方式!
public class PermisionUtils {
// Storage Permissions
public static final int REQUEST_EXTERNAL_STORAGE = 1;
public static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
public static void verifyStoragePermissions(Activity activity) {
// Check if we have write permission
int permission = ActivityCompat.checkSelfPermission(activity,
Manifest.permission.READ_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
}
}
}
使用时,只需要调verifyStoragePermissions
方法就可以了!
PermisionUtils.verifyStoragePermissions(StartingActivity.this);
在StartingActivity中也可以用实现onRequestPermissionsResult
方法,获取权限申请的结果!
由于我们是采用Shader实现的贴图,实现高斯模糊来就比较简单了~ 个是我用模糊算子!
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_texture;
uniform bool blur;
const int SHIFT_SIZE = 5; // 高斯算子左右偏移值
in vec4 blurShiftCoordinates[SHIFT_SIZE];
void main() {
if (!blur) {
outColor = texture(s_texture, v_texCoord);
} else {
// 计算当前坐标的颜色值
vec4 currentColor = texture(s_texture, v_texCoord);
vec3 sum = currentColor.rgb;
// 计算偏移坐标的颜色值总和
for (int i = 0; i < SHIFT_SIZE; i++) {
sum += texture(s_texture, blurShiftCoordinates[i].xy).rgb;
sum += texture(s_texture, blurShiftCoordinates[i].zw).rgb;
}
outColor = vec4(sum / float(2 * SHIFT_SIZE + 1), currentColor.a);
}
}
我只给GL_CLAMP_TO_EDGE
方式贴图的纹理增加了模糊效果,效果如下:
本文主要介绍了Native应用的写法,libpng图片在native的编解码使用,OpenGLES纹理的贴图及相关知识,Shader的使用等!