1. 程式人生 > >Android 為例編寫一個 OpenGL ES 3.0 例項,Native & Java 兩種實現

Android 為例編寫一個 OpenGL ES 3.0 例項,Native & Java 兩種實現

一、簡介

  • 通過這個 Sample,你將瞭解到 Android 中是怎麼使用 OpenGL ES
  • 通過繪製一個簡單的靜態三角形,來簡單入門和了解它大致的流程(類似於 HelloWorld 工程)
  • 介紹使用 Native 層Java 層 兩種方式來分別實現
  • 本文暫不介紹具體的語法,但會給比較詳細的註釋和解釋,幫助你理解

二、Native 實現

1. 標頭檔案

由於我們使用的是 OpenGL ES 3.0,所以主要使用吃標頭檔案:<GLES3/gl3.h>

2. Activity

最終還是要顯示在 Activity 上的,所以我們先準備這樣一個 Activity,它直接使用 GLSurfaceView

作為 contentView。

public class SampleActivity extends AppCompatActivity {

    private static final String TAG = "SampleActivity";
    public static final String TYPE_NAME = "type";
    public static final int TYPE_NATIVE = 0;
    public static final int TYPE_JAVA = 1;

    private GLSurfaceView mGlSurfaceView;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (!checkOpenGLES30()) { Log.e(TAG, "con't support OpenGL ES 3.0!"); finish(); } mGlSurfaceView = new GLSurfaceView(this); mGlSurfaceView.
setEGLContextClientVersion(3); mGlSurfaceView.setRenderer(getRenderer()); mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); setContentView(mGlSurfaceView); } private GLSurfaceView.Renderer getRenderer() { Intent intent = getIntent(); int type = intent.getIntExtra(TYPE_NAME, TYPE_NATIVE); Log.d(TAG, "type: " + type); GLSurfaceView.Renderer renderer; if (type == TYPE_NATIVE) { renderer = new NativeRenderer(this); } else { renderer = new JavaRenderer(this); } return renderer; } private boolean checkOpenGLES30() { ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo info = am.getDeviceConfigurationInfo(); return (info.reqGlEsVersion >= 0x30000); } @Override protected void onPause() { mGlSurfaceView.onPause(); super.onPause(); } @Override protected void onResume() { mGlSurfaceView.onResume(); super.onResume(); } }

3. Renderer

我們先介紹 NativeRenderer 的實現,如下:

public class NativeRenderer implements GLSurfaceView.Renderer {

    private Context mContext;

    static {
        System.loadLibrary("native-renderer");
    }

    public NativeRenderer(Context context) {
        mContext = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        registerAssetManager(mContext.getAssets());
        glInit();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        glResize(width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        glDraw();
    }

    public native void registerAssetManager(AssetManager assetManager);
    public native void glInit();
    public native void glResize(int width, int height);
    public native void glDraw();
}

主要定義了4個 native 的方法,需要我們在 native 層實現。

4. ShaderUtils

說實現之前,我們先寫一個工具類,負責載入和建立。工具類的作用就是可以重複使用的:

#include "ShaderUtils.h"
#include <stdlib.h>
#include "LogUtils.h"

GLuint LoadShader(GLenum type, const char *shaderSource) {
    // 1. create shader
    GLuint shader = glCreateShader(type);
    if (shader == GL_NONE) {
        LOGE("create shader failed! type: %d", type);
        return GL_NONE;
    }
    // 2. load shader source
    glShaderSource(shader, 1, &shaderSource, NULL);
    // 3. compile shared source
    glCompileShader(shader);
    // 4. check compile status
    GLint compiled;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (compiled == GL_NONE) { // compile failed
        GLint len = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
        if (len > 1) {
            char *log = static_cast<char *>(malloc(sizeof(char) * len));
            glGetShaderInfoLog(shader, len, NULL, log);
            LOGE("Error compiling shader: %s", log);
            free(log);
        }
        glDeleteShader(shader); // delete shader
        return GL_NONE;
    }
    return shader;
}

GLuint CreateProgram(const char *vertexSource, const char *fragmentSource) {
    // 1. load shader
    GLuint vertexShader = LoadShader(GL_VERTEX_SHADER, vertexSource);
    if (vertexShader == GL_NONE) {
        LOGE("load vertex shader failed! ");
        return GL_NONE;
    }
    GLuint fragmentShader = LoadShader(GL_FRAGMENT_SHADER, fragmentSource);
    if (vertexShader == GL_NONE) {
        LOGE("load fragment shader failed! ");
        return GL_NONE;
    }
    // 2. create gl program
    GLuint program = glCreateProgram();
    if (program == GL_NONE) {
        LOGE("create program failed! ");
        return GL_NONE;
    }
    // 3. attach shader
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    // we can delete shader after attach
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    // 4. link program
    glLinkProgram(program);
    // 5. check link status
    GLint linked;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if (linked == GL_NONE) { // link failed
        GLint len = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
        if (len > 1) {
            char *log = static_cast<char *>(malloc(sizeof(char) * len));
            glGetProgramInfoLog(program, len, NULL, log);
            LOGE("Error link program: %s", log);
            free(log);
        }
        glDeleteProgram(program); // delete program
        return GL_NONE;
    }
    return program;
}

char *readAssetFile(const char *filename, AAssetManager *mgr) {
    if (mgr == NULL) {
        LOGE("pAssetManager is null!");
        return NULL;
    }
    AAsset *pAsset = AAssetManager_open(mgr, filename, AASSET_MODE_UNKNOWN);
    off_t len = AAsset_getLength(pAsset);
    char *pBuffer = (char *) malloc(len + 1);
    pBuffer[len] = '\0';
    int numByte = AAsset_read(pAsset, pBuffer, len);
    LOGD("numByte: %d, len: %d", numByte, len);
    AAsset_close(pAsset);
    return pBuffer;
}

5. NativeRenderer.cpp

終於到了我們的渲染實現了,主要就是實現之前定義的那幾個 native 方法:

#include "com_afei_openglsample_NativeRenderer.h"

#include <android/asset_manager_jni.h>
#include <GLES3/gl3.h>
#include "LogUtils.h"
#include "ShaderUtils.h"

GLuint g_program;
GLint g_position_handle;
AAssetManager *g_pAssetManager = NULL;

JNIEXPORT void JNICALL Java_com_afei_openglsample_NativeRenderer_glInit
        (JNIEnv *env, jobject instance) {
    char *vertexShaderSource = readAssetFile("vertex.vsh", g_pAssetManager);
    char *fragmentShaderSource = readAssetFile("fragment.fsh", g_pAssetManager);
    g_program = CreateProgram(vertexShaderSource, fragmentShaderSource);
    if (g_program == GL_NONE) {
        LOGE("gl init failed!");
    }
    // vPosition 是在 'vertex.vsh' 檔案中定義的
    GLint g_position_handle =glGetAttribLocation(g_program, "vPosition");
    LOGD("g_position_handle: %d", g_position_handle);
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // 背景顏色設定為黑色 RGBA (range: 0.0 ~ 1.0)
}

JNIEXPORT void JNICALL Java_com_afei_openglsample_NativeRenderer_glResize
        (JNIEnv *env, jobject instance, jint width, jint height) {
    glViewport(0, 0, width, height); // 設定視距視窗
}

JNIEXPORT void JNICALL Java_com_afei_openglsample_NativeRenderer_glDraw
        (JNIEnv *env, jobject instance) {
    GLint vertexCount = 3;
    // OpenGL的世界座標系是 [-1, -1, 1, 1]
    GLfloat vertices[] = {
            0.0f, 0.5f, 0.0f, // 第一個點(x, y, z)
            -0.5f, -0.5f, 0.0f, // 第二個點(x, y, z)
            0.5f, -0.5f, 0.0f // 第三個點(x, y, z)
    };
    glClear(GL_COLOR_BUFFER_BIT); // clear color buffer
    // 1. 選擇使用的程式
    glUseProgram(g_program);
    // 2. 載入頂點資料
    glVertexAttribPointer(g_position_handle, vertexCount, GL_FLOAT, GL_FALSE, 3 * 4, vertices);
    glEnableVertexAttribArray(g_position_handle);
    // 3. 繪製
    glDrawArrays(GL_TRIANGLES, 0, vertexCount);
}

JNIEXPORT void JNICALL Java_com_afei_openglsample_NativeRenderer_registerAssetManager
        (JNIEnv *env, jobject instance, jobject assetManager) {
    if (assetManager) {
        g_pAssetManager = AAssetManager_fromJava(env, assetManager);
    } else {
        LOGE("assetManager is null!")
    }
}

6. CMakeLists.txt

當然,它也是或不可少的,負責編譯我們的 native 動態庫。

cmake_minimum_required(VERSION 3.4.1)

include_directories( ${CMAKE_SOURCE_DIR}/src/main/cpp/inc )

add_library( native-renderer
             SHARED
             src/main/cpp/src/ShaderUtils.cpp
             src/main/cpp/src/com_afei_openglsample_NativeRenderer.cpp )

target_link_libraries( native-renderer
                       # for 'AAssetManager_fromJava'
                       android
                       # for opengl es 3.0 library
                       GLESv3
                       # for log library
                       log )

7. vertex.vsh 和 fragment.fsh

我們將頂點著色器和片元著色器的程式碼放在了 assets 目錄下,實現分別為:

vertex.vsh

第一行是宣告使用的版本,這裡我們只是簡單的將外面的輸入,直接傳給了 gl_Position

#version 300 es

layout(location = 0) in vec4 vPosition;

void main() {
    gl_Position = vPosition;
}

fragment.fsh

同樣第一行是宣告使用的版本,然後繪製的顏色直接使用紅色,即 RGBA (range: 0.0 ~ 1.0)

#version 300 es

precision mediump float;
out vec4 fragColor;

void main() {
    fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );
}

8. 執行效果

在這裡插入圖片描述

三、Java 實現

1. Activity

和 Native 實現的程式碼一樣,唯一的區別是使用 JavaRenderer 類作為渲染。

2. JavaRenderer

程式碼來看其實和 Native 層的極其類似,畢竟只是使用 Java 包了一層。

public class JavaRenderer implements GLSurfaceView.Renderer {

    private static final String TAG = "JavaRenderer";
    private Context mContext;
    private int mProgram;
    private int mPositionHandle;

    public JavaRenderer(Context context) {
        mContext = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        String vertexSource = ShaderUtils.loadFromAssets("vertex.vsh", mContext.getResources());
        String fragmentSource = ShaderUtils.loadFromAssets("fragment.fsh", mContext.getResources());
        mProgram = ShaderUtils.createProgram(vertexSource, fragmentSource);
        // vPosition 是在 'vertex.vsh' 檔案中定義的
        mPositionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
        Log.d(TAG, "mPositionHandle: " + mPositionHandle);
        // 背景顏色設定為黑色 RGBA (range: 0.0 ~ 1.0)
        GLES30.glClearColor(0, 0, 0, 1);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // 視距區域設定使用 GLSurfaceView 的寬高
        GLES30.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        int vertexCount = 3;
        // OpenGL的世界座標系是 [-1, -1, 1, 1]
        float[] vertices = new float[]{
                0.0f, 0.5f, 0, // 第一個點(x, y, z)
                -0.5f, -0.5f, 0, // 第二個點(x, y, z)
                0.5f, -0.5f, 0 // 第三個點(x, y, z)
        };
        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4); // 一個 float 是四個位元組
        vbb.order(ByteOrder.nativeOrder()); // 必須要是 native order
        FloatBuffer vertexBuffer = vbb.asFloatBuffer();
        vertexBuffer.put(vertices);
        vertexBuffer.position(0); // 這一行不要漏了

        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); // clear color buffer
        // 1. 選擇使用的程式
        GLES30.glUseProgram(mProgram);
        // 2. 載入頂點資料
        GLES30.glVertexAttribPointer(mPositionHandle, vertexCount, GLES30.GL_FLOAT, false, 3 * 4, vertexBuffer);
        GLES30.glEnableVertexAttribArray(mPositionHandle);
        // 3. 繪製
        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vertexCount);
    }

}

3. ShaderUtils

同樣,我們將公共的方法抽離為一個工具類,並且,程式碼也是和 Native 層的極其類似。

public class ShaderUtils {

    public static final String TAG = "ShaderUtils";

    public static int loadShader(int type, String source) {
        // 1. create shader
        int shader = GLES30.glCreateShader(type);
        if (shader == GLES30.GL_NONE) {
            Log.e(TAG, "create shared failed! type: " + type);
            return GLES30.GL_NONE;
        }
        // 2. load shader source
        GLES30.glShaderSource(shader, source);
        // 3. compile shared source
        GLES30.glCompileShader(shader);
        // 4. check compile status
        int[] compiled = new int[1];
        GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == GLES30.GL_NONE) { // compile failed
            Log.e(TAG, "Error compiling shader. type: " + type + ":");
            Log.e(TAG, GLES30.glGetShaderInfoLog(shader));
            GLES30.glDeleteShader(shader); // delete shader
            shader = GLES30.GL_NONE;
        }
        return shader;
    }

    public static int createProgram(String vertexSource, String fragmentSource) {
        // 1. load shader
        int vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == GLES30.GL_NONE) {
            Log.e(TAG, "load vertex shader f