Android 图形显示系统(十四)OpenGLES 纯Native实现PNG图片贴图

文章目录

    • 如何实现一个纯Native的应用
      • 创建项目
      • AndroidManifest的配置
      • 添加Native层的代码
      • 简介一下NativeActivity
    • 加载PNG图片
      • png库的编译
      • png库的使用
    • OpenGLES贴图
      • 创建纹理Texture
      • 使用纹理
    • Shader的那些事
    • SD卡的读写权限问题
    • 给纹理增加高斯模糊
    • 小结

#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

        
            
            
                
                
            
        

在一个项目中,java代码和Native代码是可以共存的,如果没有java代码,将android:hasCode设置为false。

    

添加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 

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,前者表示“使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色”,后者表示“使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色”。通过下图很好的解释这两者差别:
    Android 图形显示系统(十四)OpenGLES 纯Native实现PNG图片贴图_第1张图片

使用纹理

创建好的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这是 混合写法,将纹理的坐标和屏幕的坐标一一对应,是不是很不好理解,我们来个图,一个就明白了!

Android 图形显示系统(十四)OpenGLES 纯Native实现PNG图片贴图_第2张图片

两个坐标系的方向不一样,所以vVertices看起来怪怪的,要加以区分!

indices是绘制的顺序,OpenGLES最复杂只能按照三角形进行渲染,所以这里的索引就是告诉GPU,前面定义的vVertices中顶点,怎么组成三角形。indices中定义的索引将按照下图的方式进行渲染。

Android 图形显示系统(十四)OpenGLES 纯Native实现PNG图片贴图_第3张图片

  • 顶点数据加载
    注意,我们这里屏幕顶点数据,单个点的长度是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官网~
Android 图形显示系统(十四)OpenGLES 纯Native实现PNG图片贴图_第4张图片

代码实现:

    // 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);

我们来看看我们示例中的效果!
Android 图形显示系统(十四)OpenGLES 纯Native实现PNG图片贴图_第5张图片

纹理所用原图

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中申明需要的权限
    
    
    
  • 申明用旧的SD卡权限方式
    

之前是不用申明的,是因为在新的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方式贴图的纹理增加了模糊效果,效果如下:
Android 图形显示系统(十四)OpenGLES 纯Native实现PNG图片贴图_第6张图片

小结

本文主要介绍了Native应用的写法,libpng图片在native的编解码使用,OpenGLES纹理的贴图及相关知识,Shader的使用等!

你可能感兴趣的:(Graphics)