Android 圖形顯示系統(十四)OpenGLES 純Native實現PNG圖片貼圖


#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_gluenative_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_NEARESTGL_LINEAR,前者表示“使用紋理中座標最接近的一個像素的顏色作爲需要繪製的像素顏色”,後者表示“使用紋理中座標最接近的若干個顏色,通過加權平均算法得到需要繪製的像素顏色”。通過下圖很好的解釋這兩者差別:
    Texture的Filter Mode

使用紋理

創建好的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中定義的索引將按照下圖的方式進行渲染。

Texture座標索引

  • 頂點數據加載
    注意,我們這裏屏幕頂點數據,單個點的長度是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);

這裏的01是前面加載座標是對應的索引,和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的使用等!

發佈了21 篇原創文章 · 獲贊 36 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章