文章目錄
#OpenGLES 純Native實現PNG圖片貼圖
春節臨近本來不想更了,但是爲了紀念即將逝去的一年,還是留下點什麼吧!就讓我們用OpenglES實現一個純native的png圖片的貼圖!
如何實現一個純Native的應用
我採用的是Android Studio!Android提供了NativeActivity
來實現純Native應用,我們將Native的實現打包成一個共享庫,通過NativeActivity
來調對應的共享庫。創建一個native的應用,和正常項目大同小異,總的來說,主要注意一下幾個部分:
創建項目
時選擇 Native C++,C++ 標準我採用的是C++ 14
AndroidManifest的配置
添加Activity,名稱就是android.app.NativeActivity
,主要的就是這裏的meta-data
,native打包成共享庫opengles_simples
。
<activity android:name="android.app.NativeActivity">
<meta-data
android:name="android.app.lib_name"
android:value="opengles_simples" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
在一個項目中,java代碼和Native代碼是可以共存的,如果沒有java代碼,將android:hasCode
設置爲false。
<application
android:hasCode="true"
... ...
添加Native層的代碼
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 <android_native_app_glue.h>
void android_main(struct android_app *app) {
//Empty
}
- CMakeList.txt的編寫
CMake沒有細研究,將就看吧!
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的入口,如果沒有是會報錯的!
簡介一下NativeActivity
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);
- userData
用戶自己的數據,用戶自己可以隨心所欲定義 - onAppCmd
App的狀態處理,支持的cmd定義在android_native_app_glue.h中,包括:
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,
};
- onInputEvent
對輸入事件的回調,支持KEY事件和MOTION事件。
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
,能夠滿足所有開發的需求!!!
加載PNG圖片
Native中沒有現成的圖片加載(編解碼),但是我們可以用libpng等庫來滿足我們的需求!png相關的內容,前面我已經有文章整理過,可以參考 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庫的使用
採用的結構和函數如下,具體的實現看代碼吧!
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
中。
OpenGLES貼圖
OpenGL不多贅述,我們可以通過圖片貼圖來讓OpenGL變的斑斕燦爛。我們就來看看怎麼實現貼圖吧!
創建紋理Texture
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一塊內存一樣,這裏是在
GPU中;glBindTexture
是將紋理進行綁定,將申請的Texture和一個2D的紋理進行綁定;glTexImage2D
將圖片數據加載到紋理中,包括數據的描述和數據pixels
。- Filter模式,
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
中定義的索引將按照下圖的方式進行渲染。
- 頂點數據加載
注意,我們這裏屏幕頂點數據,單個點的長度是4,而紋理的長度爲2。紋理座標加載時,在vVertices中有一個偏移。
// 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 | 重複紋理和鏡像紋理 |
效果圖如下,圖片來源OpenGL官網~
代碼實現:
// 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和片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);
SD卡的讀寫權限問題
Android的權限管理越來越嚴格和精細化,好久沒有寫應用,發現SD的讀寫權限申請也很麻煩,看了半天源碼,幸好還有補救的辦法!這裏記下以備後續之需!
之前老是想從native直接申請權限,後來發現行不通,所以轉而回到Java層!我們增加一個啓動界面StartingActivity
,在StartingActivity
中申請權限,獲取到權限後立即啓動NativeActivity
,關掉StartingActivity
,這樣問題就可以完美解決!
總的說來,申請權限需要注意以下幾點:
- 在Manifest中申明需要的權限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
... ...
- 申明用舊的SD卡權限方式
<application
android:requestLegacyExternalStorage="true"
... ...
之前是不用申明的,是因爲在新的Android版本,又有新的SD卡讀寫方式!
- 動態權限獲取
我們這裏自定義了一個PermisionUtils來實現!
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的使用等!