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

[TOC]

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 重复纹理和镜像纹理
GL_CLAMP_TO_BORDER gl32边缘色,不拉伸

效果图如下,图片来源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的使用等!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容