2022.07.20 NDK OpenGL ES 3.0 :画个三角形,纹理贴图(刚入门就入土)

老板要求做一个用shader渲染图像的Android app,毕竟是安卓,恰好OpenGL ES也提供了Java接口,Github上大部分代码都是Java实现的,在同一中语言体系下想要画三角等入门还是方便,但其实呢,还是建议用C++来编写OpenGL相关的代码,原因有二:1. OpenGL接口的官方教程是基于C++的,初学者查函数调用很方便;2. 我是做图像处理,万一说不好用到类似OpenCV库呢。。。于是,我被折磨了两天入门,这篇博客主要记录一些NDK实现逻辑,以及如何实现一些基于NDK的OpenGL基础渲染效果。

Reference for OpenGL ES based on NDK

首先,学习OpenGL接口方面,强推learnOpenGL,每一个知识点都有Example且配有源码,循序渐进,能通过案例慢慢地讲清楚很多初学者疑惑的地方。
另外,很感谢以下博主的分享, 在我自闭的时候提供了太多帮助,毕竟网上资料真的不多。
也能优先参考他们的代码,我不过是代码的搬运工哈哈~:

  1. 字节流动代佬,提供一整个基于NDK的OpenGL教程,但项目集成度太高我目前只是用作参考。
  2. 通俗易懂的OpenGL,github代码可运行,挺不错的,看上传的github代码能看出参考了字节流动和learnOpenGL的部分,有设置背景和画三角demo,代码中有个问题是屏幕翻转后图案消失,这也是我现在遗留的问题,mark。
  3. NDK实现纹理贴图,代码大部分都是对的,对纹理贴图实现帮助很大。

NDK & JNI(Native)

搭环境,简单画个三角

NDK

文件目录

在这里插入图片描述

Java部分

  • MainActivity: 主程序入口,调用两个基本类能够通过OpenGL ES的API来创建图形程序。
  • NativeImpl: 声明类似public static native类型的函数去实现Renderer类的三个方法,意味着只有声明,定义在cpp文件中。
MainActivity.java

主要写3个类:MainActivity, MyGLSurfaceView,MyRenderer ,主程序显示MyGLSurfaceView的渲染内容,具体渲染实现由MyRenderer定义。GLSurfaceView.Renderer派生类需要重写三个方法:onSurfaceCreated(初始化),onSurfaceChanged(屏幕窗口大小变化),onDrawFrame(渲染),这三个方法是用C++实现,所以这三个方法是对NativeImpl类相应方法的简单调用。

package com.example.c_ndktest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.opengl.GLSurfaceView;
import android.os.Bundle;

import com.example.c_ndktest.databinding.ActivityMainBinding;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class MainActivity extends AppCompatActivity {
    private GLSurfaceView gLView;
    private MyRenderer renderer;

    // public MyRenderer mrenderer;

    // Used to load the 'c_ndktest' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gLView = new MyGLSurfaceView(this);
        setContentView(gLView);
    }

    /**
     * A native method that is implemented by the 'c_ndktest' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    private class MyGLSurfaceView extends GLSurfaceView {
        public MyGLSurfaceView(Context context) {
            super(context);
            setEGLContextClientVersion(3);
            renderer = new MyRenderer();
            setRenderer(renderer);
        }
    }

    private class MyRenderer implements GLSurfaceView.Renderer {
        @Override
        public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
            NativeImpl.init();
        }

        @Override
        public void onSurfaceChanged(GL10 gl10, int width, int height) {
            NativeImpl.OnSurfaceChanged(width, height);
        }

        @Override
        public void onDrawFrame(GL10 gl10) {
            NativeImpl.draw();
        }
    }
}

NativeImpl.java
package com.example.c_ndktest;

public class NativeImpl {
    static {
        System.loadLibrary("native-lib");
    }
    public static native String stringFromJNI();

    public static native void init();
    public static native void OnSurfaceChanged(int width,int height);
    public static native void draw();
}

static部分写C文件生成的动态库名,调用他们,在CMakeLists中定义生成库的名字。在这里插入图片描述

若没有找到定义会报错,定义完后,左侧应该会显示cpp文件的符号,点击跳转过去就是C实现。
在这里插入图片描述
在这里插入图片描述

C++部分:

  • native-lib.cpp: Native层实现java函数的定义,有一定命名规则,这样java才能找到这个函数。
  • demo/TriangleDemo.cpp or .h:渲染三角主程序,调用OpenGL的接口实现。
  • util/* : 载入Shade的一些辅助函数,还有log函数。
  • CMakeLists.txt : 写链接的其他库,需要链接源文件,生成target等。
native-lib.cpp

首先include实现渲染的类的头文件TriangleDemo.h,定义一个全局的类对象trangleDemo,Native层先简单调用成员函数实现接口。可以看到命名规则为:包名_Java类名_方法名(){}。

#include <jni.h>
#include <string>
#include "demo/TriangleDemo.h"

TriangleDemo *triangleDemo = NULL;

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_c_1ndktest_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject clazz) {
    std::string hello = "Hello NativeImpl from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_c_1ndktest_NativeImpl_init(JNIEnv *env, jclass instance) {
    if (triangleDemo == NULL) {
        triangleDemo = new TriangleDemo();
    }
    triangleDemo->Init();
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_c_1ndktest_NativeImpl_OnSurfaceChanged(JNIEnv *env, jclass instance,jint width, jint height) {
    triangleDemo->OnSurfaceChanged(width,height);
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_c_1ndktest_NativeImpl_draw(JNIEnv *env, jclass instance) {
    triangleDemo->draw();
}

TriangleDemo.h

在类中定义一些句柄成员变量,默认为0是NULL,成员函数就是三个方法,另外还有一个顶点数组对象VAO和顶点缓冲对象VBO。

#ifndef C_NDKTEST_TRIANGLEDEMO_H
#define C_NDKTEST_TRIANGLEDEMO_H

#include <GLES3/gl3.h>

class TriangleDemo {
public:
    TriangleDemo(){
        m_ProgramObj = 0;
        m_VertexShader = 0;
        m_FragmentShader = 0;
    };
    void Init();
    void draw();
    void OnSurfaceChanged(int width, int height);

    GLuint m_ProgramObj;
    GLuint m_VertexShader;
    GLuint m_FragmentShader;
    GLuint VBO, VAO;
};


#endif //C_NDKTEST_TRIANGLEDEMO_H
TriangleDemo.cpp

终于到真正的渲染程序了!太不容易了。。

TriangleDemo::Init()

  1. 设定背景颜色
  2. 编译链接vertex shader和fragment shader,与glprogram链接,然后通过glUseProgram(m_ProgramObj)使用程序,注意这里是只有一个程序的情况,如果有多个程序,例如对不同图元编译不同的shader,需要在draw()中写这个函数用到时再启用程序。
    Every shader and rendering call after glUseProgram will now use this program object (and thus the shaders
  3. 生成VAO和VBO,有这俩东西的原因是多次从CPU向GPU传输数据是很慢的,因此把多个顶点数据存入VBO对象,一次性传入GPU(存入VBO就相当于传入了GPU)并存在里面,可以节省时间,而VAO用于管理多个VBO,在draw()时,通常一次性画出来一个VAO中的所有内容,如果想分批次画,则要存在不同VAO里
    在这里插入图片描述

生成VAO和VBO的流程

  1. 定义GLuint类型的VAO,VBO,我已经设在成员函数里就不管了;
  2. glGenVertexArrays和glGenBuffer生成数据和缓冲对象的ID;
  3. 因为OpenGL中同一个缓冲区ID可以对应多个不同类型的缓冲对象,因此,先 glBindBuffer(GL_ARRAY_BUFFER, VBO); 绑定一个类型到ID上,bind函数其实也有启用这个对象的意思,意味着后面填数据或者给shader中属性幅值都是用的这个VBO和VAO;
  4. 然后向这个定义好的buffer中填数据 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
  5. 用这个buffer中的数据给shader属性变量赋值 glVertexAttribPointer,参数挺多的就说2点:1. 因为已经绑定VBO,最后一个参数要写在VBO中初始取数据的地址: This is the offset of where the position data begins in the buffer. Since the position data is at the start of the data array this value is just 0. 如第一个vertex position是从0位置开始,而vertex color属性要偏移3个float开始取;2. stride是下一次取数据要跳几个字节,因为VBO中每个顶点都有6个float类型属性参数,因此跳6个float。
  6. 启用属性 glEnableVertexAttribArray。

多个program配多个VAO的Example:
在这里插入图片描述

TriangleDemo::OnSurfaceChanged()

不说了,我没定义变换矩阵,就简单赋值。

TriangleDemo::draw()
  1. glClearColor 画背景
  2. glUseProgram 启用指定的shader程序
  3. glBindVertexArray 启用指定的VAO
  4. glDrawArrays 画顶点,数组参数是从第几个顶点开始画几个
  5. 绑定VAO ID为0,就是取消绑定的意思
#include "../util/logUtil.h"
#include "../util/GLUtil.h"
#include "TriangleDemo.h"
#include <GLES3/gl3.h>

void TriangleDemo::draw() {
    if(m_ProgramObj == 0)
        return;

    glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(1.0, 0.5, 1.0, 1.0);

    glUseProgram (m_ProgramObj);

    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glBindVertexArray(0);
}

void TriangleDemo::Init() {
    if (m_ProgramObj != 0) return;

    char vShaderStr[] =
            "#version 300 es                          \n"
            "layout(location = 0) in vec3 vPosition;  \n"
            "layout(location = 1) in vec3 vColor;     \n"
            "out vec3 ourColor;                            \n"
            "void main()                              \n"
            "{                                        \n"
            "   gl_Position = vec4(vPosition.xyz, 1.0);              \n"
            "   ourColor = vColor; \n"
            "}                                        \n";

    char fShaderStr[] =
            "#version 300 es                              \n"
            "precision mediump float;                     \n"
            "out vec4 fragColor;                          \n"
            "in vec3 ourColor;                          \n"
            "void main()                                  \n"
            "{                                            \n"
            "   fragColor = vec4 ( ourColor.rgb, 1.0 );  \n"
            "}                                            \n";

    LOGCATE("init BgDemo");

    float vertices[] = {
            // positions         // colors
            0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // bottom right
            -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // bottom left
            0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // top
    };

    glClearColor(1.0, 0.5, 1.0, 1.0);
    m_ProgramObj =  GLUtil::CreateProgram(vShaderStr, fShaderStr, m_VertexShader, m_FragmentShader);
    glUseProgram (m_ProgramObj);

    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
}

void TriangleDemo::OnSurfaceChanged(int width, int height) {
    LOGCATE("MyGLRenderContext::OnSurfaceChanged [w, h] = [%d, %d]", width, height);
    glViewport(0, 0, width, height);
}
GLUtil.h
#ifndef OPENGLDEMO_GLUTIL_H
#define OPENGLDEMO_GLUTIL_H
#include <GLES3/gl3.h>

class GLUtil {

public:
    static GLuint CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource,
                                      GLuint &vertexShaderHandle,
                                      GLuint &fragShaderHandle);
    static GLuint LoadShader(GLenum shaderType, const char *pSource);

    static void CheckGLError(const char *pGLOperation);

    static  void  test();
};


#endif //OPENGLDEMO_GLUTIL_H
GLUtil.cpp

主要是写编译shader的过程,不赘述。

//
// Created by Lai on 2020/11/8.
//

#include <malloc.h>
#include "GLUtil.h"
#include "logUtil.h"


GLuint GLUtil::CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource,
                             GLuint &vertexShaderHandle, GLuint &fragShaderHandle) {
    GLuint program = 0;
    FUN_BEGIN_TIME("GLUtils::CreateProgram");
    vertexShaderHandle = LoadShader(GL_VERTEX_SHADER, pVertexShaderSource);
    if (!vertexShaderHandle) return program;
    fragShaderHandle = LoadShader(GL_FRAGMENT_SHADER, pFragShaderSource);
    if (!fragShaderHandle) return program;

    program = glCreateProgram();
    if (program)
    {
        glAttachShader(program, vertexShaderHandle);
        CheckGLError("glAttachShader");
        glAttachShader(program, fragShaderHandle);
        CheckGLError("glAttachShader");
        glLinkProgram(program);
        GLint linkStatus = GL_FALSE;
        // Returns the requested object parameter.
        glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);

        // Detaches a shader object from a program object to which it is attached
        // after compiling the shader, delete it.
        glDetachShader(program, vertexShaderHandle);
        glDeleteShader(vertexShaderHandle);
        vertexShaderHandle = 0;
        glDetachShader(program, fragShaderHandle);
        glDeleteShader(fragShaderHandle);
        fragShaderHandle = 0;
        if (linkStatus != GL_TRUE)
        {
            GLint bufLength = 0;
            glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufLength);
            if (bufLength)
            {
                char* buf = (char*) malloc((size_t)bufLength);
                if (buf)
                {
                    glGetProgramInfoLog(program, bufLength, NULL, buf);
                    LOGCATE("GLUtils::CreateProgram Could not link program:\n%s\n", buf);
                    free(buf);
                }
            }
            glDeleteProgram(program);
            program = 0;
        }
    }
    FUN_END_TIME("GLUtils::CreateProgram")
    LOGCATE("GLUtils::CreateProgram program = %d", program);
    return program;
}

void GLUtil::test() {
    LOGCATE("TEST!!!!!");
}

GLuint GLUtil::LoadShader(GLenum shaderType, const char *pSource)
{
    GLuint shader = 0;
    FUN_BEGIN_TIME("GLUtils::LoadShader")
        shader = glCreateShader(shaderType);
        if (shader)
        {
            glShaderSource(shader, 1, &pSource, NULL);
            glCompileShader(shader);
            GLint compiled = 0;
            glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
            if (!compiled)
            {
                GLint infoLen = 0;
                glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
                if (infoLen)
                {
                    char* buf = (char*) malloc((size_t)infoLen);
                    if (buf)
                    {
                        glGetShaderInfoLog(shader, infoLen, NULL, buf);
                        LOGCATE("GLUtils::LoadShader Could not compile shader %d:\n%s\n", shaderType, buf);
                        free(buf);
                    }
                    glDeleteShader(shader);
                    shader = 0;
                }
            }
        }
    FUN_END_TIME("GLUtils::LoadShader")
    return shader;
}


void GLUtil::CheckGLError(const char *pGLOperation)
{
    for (GLint error = glGetError(); error; error = glGetError())
    {
        LOGCATE("GLUtils::CheckGLError GL Operation %s() glError (0x%x)\n", pGLOperation, error);
    }

}
logUtil.h

日志宏定义,还挺重要的。

#ifndef OPENGLDEMO_LOGUTIL_H
#define OPENGLDEMO_LOGUTIL_H

#include<android/log.h>
#include <sys/time.h>


#define  LOG_TAG "OpenGlDemo"

#define  LOGCATE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define  LOGCATV(...)  __android_log_print(ANDROID_LOG_VERBOSE,LOG_TAG,__VA_ARGS__)
#define  LOGCATD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define  LOGCATI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)

#define FUN_BEGIN_TIME(FUN) {\
    LOGCATE("%s:%s func start", __FILE__, FUN); \
    long long t0 = GetSysCurrentTime();

#define FUN_END_TIME(FUN) \
    long long t1 = GetSysCurrentTime(); \
    LOGCATE("%s:%s func cost time %ldms", __FILE__, FUN, (long)(t1-t0));}

#define BEGIN_TIME(FUN) {\
    LOGCATE("%s func start", FUN); \
    long long t0 = GetSysCurrentTime();

#define END_TIME(FUN) \
    long long t1 = GetSysCurrentTime(); \
    LOGCATE("%s func cost time %ldms", FUN, (long)(t1-t0));}

static long long GetSysCurrentTime()
{
    struct timeval time;
    gettimeofday(&time, NULL);
    long long curTime = ((long long)(time.tv_sec))*1000+time.tv_usec/1000;
    return curTime;
}

#define GO_CHECK_GL_ERROR(...)   LOGCATE("CHECK_GL_ERROR %s glGetError = %d, line = %d, ",  __FUNCTION__, glGetError(), __LINE__)

#define DEBUG_LOGCATE(...) LOGCATE("DEBUG_LOGCATE %s line = %d",  __FUNCTION__, __LINE__)

#endif //OPENGLDEMO_LOGUTIL_H

CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.18.1)

# Declares and names the project.

project("c_ndktest")

file(GLOB_RECURSE ALL_SOURCE "*.cpp", "*.h", "*.h")

include_directories(
        util
        demo
)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
        native-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        # native-lib.cpp
        ${ALL_SOURCE}
        demo/TriangleDemo.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        native-lib

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib}
        GLESv3)

注意,修改文件源文件后要同步编译一下,否则会出现未定义错误:

在这里插入图片描述
一般NDK中出现未定义就两种情况:1.没在CMake里写外部链接库,要在target_link_libraries中写;2.没同步,要手动随便修改一下CMake,同步一下就好。

实现效果

在这里插入图片描述

引入外部shader文件

这里还有一个问题是我们的shader是写成字符串形式,对书写很不方便,想引入外部文件。
首先说明一下,通过c++ fstream文件读取方式基本不现实,因为他是以生成的target为根目录找文件,你怎么知道target在哪,错误的文件路径就找不到文件。
这里通过AssetManager取数据,文件必须放在assets目录下

在这里插入图片描述
大致思路是,在C++中获取Java AssetManager对象,然后调用AAssetManager_open和AAsset_read函数读取文件。
先来看Java修改部分,我想的是在初始化时,先将AssetManager对象保存为MainActivity私有变量,防止空引用

在这里插入图片描述
随后在NativeImpl中增加一个接口传入AssetManager对象

在这里插入图片描述
这个接口函数在MyRenderer.onSurfaceCreated()被调用

在这里插入图片描述

这样,mrg对象就传入c++了。

然后 来看c++修改部分,先定义Native层的接口函数,将mrg对象保存在全局变量g_mrg。
在这里插入图片描述
util目录下,新建一个ReadFileUtil.h文件,写使用AssetManager传数据的函数 LoadFileContent(),传出unsigned char*数据地址。

#ifndef C_NDKTEST_READFILEUTIL_H
#define C_NDKTEST_READFILEUTIL_H
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>


const unsigned char *LoadFileContent(const char *filepath);
AAssetManager *g_mrg;

const unsigned char *LoadFileContent(const char *filepath) {
    // read shader code form asset
    unsigned char *fileContent = nullptr;
    AAsset *asset = AAssetManager_open(g_mrg, filepath, AASSET_MODE_UNKNOWN);
    if (asset == nullptr) {
        // LOGE("LoadFileContent asset is null, load shader error ");
        LOGCATE("LoadFileContent asset is null, load shader error ");
    }
    int filesSize_v = AAsset_getLength(asset);
    fileContent = new unsigned char[filesSize_v];
    AAsset_read(asset, fileContent, filesSize_v);
    fileContent[filesSize_v] = '\0';
    AAsset_close(asset);

    return fileContent;
}

#endif //C_NDKTEST_READFILEUTIL_H

随后我们在初始化函数中使用上面定义的函数把shader的字符数据存起来就好了

在这里插入图片描述

TriangleDemo类新增成员函数getShaderCode

在这里插入图片描述

至此,shader文件就成功读入了~

简单贴个texture到方块上

老样子,来看看新的目录结构吧,新添加TextureDemo类,另外需要在ReadFileUtil.h中添加读图片的方法,修改native-lib.cpp确保java调用新的方法。
在这里插入图片描述

TextureDemo.h
#ifndef C_NDKTEST_TEXTUREDEMO_H
#define C_NDKTEST_TEXTUREDEMO_H
#include <GLES3/gl3.h>

class TextureDemo {
public:
    TextureDemo(){
        m_ProgramObj = 0;
        m_VertexShader = 0;
        m_FragmentShader = 0;
    };
    void Init();
    void draw();
    void OnSurfaceChanged(int width, int height);
    void getShaderCode(const char *cs, int flag);
    void getTexturedata(unsigned char *data, int width, int height);

    GLuint m_ProgramObj;
    GLuint m_VertexShader;
    GLuint m_FragmentShader;
    GLuint VBO, VAO, EBO;
    const char *vs;
    const char *fs;
    unsigned char *texturedata;
    int texturewidth, textureheight;
    unsigned int texture;

};

#endif //C_NDKTEST_TEXTUREDEMO_H

TextureDemo.cpp

渲染程序主要有两个地方要改,添加EBO对象,画texture,另外因为两个三角拼成一个方形,要画4个顶点。

我们知道复杂图案都是用三角形拼出来的,EBO就是用来告诉程序,在VBO存储的众多顶点中,用哪几个顶点一组画三角形,简单说就是存储顶点序号的buffer。注意与坐标buffer区别:

  1. 类型是unsigned int indices[] = {…}
  2. 在bind VAO之后,bindbuffer类型是glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

在这里插入图片描述

  1. draw()中,改为调用 glDrawElements()

画texture,主要添加纹理坐标系下,顶点对应的坐标点texture coords,这样只要这个像素点在图元内,图元中间的像素值会在顶点之间坐标的插值处采样而来。方法也是生成绑定那一套,不多说了。

#include "../util/logUtil.h"
#include "../util/GLUtil.h"
#include "TextureDemo.h"
#include <GLES3/gl3.h>


void TextureDemo::draw() {
    if(m_ProgramObj == 0)
        return;

    glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(0.5, 0.5, 1.0, 1.0);

    glUseProgram (m_ProgramObj);

    glBindTexture(GL_TEXTURE_2D, texture);
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);
}

void TextureDemo::Init() {
    if (m_ProgramObj != 0) return;

    LOGCATE("init BgDemo");

    float vertices[] = {
            // positions          // colors           // texture coords
            1.0f,  1.0f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // top right
            1.0f, -1.0f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // bottom right
            -1.0f, -1.0f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // bottom left
            -1.0f,  1.0f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // top left
    };
    // 注意是uint
    unsigned int indices[] = {
            0, 1, 3,
            1, 2, 3
    };

    glClearColor(0.5, 0.5, 1.0, 1.0);
    m_ProgramObj =  GLUtil::CreateProgram(vs, fs, m_VertexShader, m_FragmentShader);
    glUseProgram (m_ProgramObj);

    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 注意是elemet_array_buffer类型
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // trans data to shader program
    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    // texcoord attribute
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);

    // texture
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texturewidth, textureheight, 0, GL_RGB, GL_UNSIGNED_BYTE, texturedata);
    glGenerateMipmap(GL_TEXTURE_2D);

}

void TextureDemo::OnSurfaceChanged(int width, int height) {
    LOGCATE("MyGLRenderContext::OnSurfaceChanged [w, h] = [%d, %d]", width, height);
    glViewport(0, 0, width, height);
}

void TextureDemo::getShaderCode(const char *cs, int flag) {
    if (flag == 0){
        vs = cs;
    }else{
        fs = cs;
    }
}

void TextureDemo::getTexturedata(unsigned char *data, int width, int height){
    texturedata = data;
    texturewidth = width;
    textureheight = height;
}
ReadFileUtil.h

发现读bmp文件不用做什么其他decode操作,比较方便,建议纹理转成bmp文件比较好。
另外我第一次显示图片时,会错误地将彩色显示成黑白,将图片的长宽改成2的倍数就不会出错。

#ifndef C_NDKTEST_READFILEUTIL_H
#define C_NDKTEST_READFILEUTIL_H
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#define STB_IMAGE_IMPLEMENTATION


const unsigned char *LoadFileContent(const char *filepath);
unsigned char *DecodeBMP(unsigned char *bmpFileData, int &width, int &height);
unsigned char *ReadBMP(const char *bmpPath, int &width, int &height);
AAssetManager *g_mrg;

const unsigned char *LoadFileContent(const char *filepath) {
    // read shader code form asset
    unsigned char *fileContent = nullptr;
    AAsset *asset = AAssetManager_open(g_mrg, filepath, AASSET_MODE_UNKNOWN);
    if (asset == nullptr) {
        // LOGE("LoadFileContent asset is null, load shader error ");
        LOGCATE("LoadFileContent asset is null, load shader error ");
    }
    int filesSize_v = AAsset_getLength(asset);
    fileContent = new unsigned char[filesSize_v];
    AAsset_read(asset, fileContent, filesSize_v);
    fileContent[filesSize_v] = '\0';
    AAsset_close(asset);

    return fileContent;
}

unsigned char *DecodeBMP(unsigned char *bmpFileData, int &width, int &height) {
    if (0x4D42 == *((unsigned short *) bmpFileData)) { // 数据头是否为0x4D42 判断是否是 24 位的位图,
        // 读格式头
        int pixelDataOffset = *((int *) (bmpFileData + 10));// 取出像素数据在内存块的偏移地址
        width = *((int *) (bmpFileData + 18));
        height = *((int *) (bmpFileData + 22));
        unsigned char *pixelData = bmpFileData + pixelDataOffset;
        // 位图像素数据是 BGR 排布的,所以更换 r b 的位置
        for (int i = 0; i < width * height * 3; i += 3) {
            unsigned char temp = pixelData[i];
            pixelData[i] = pixelData[i + 2];
            pixelData[i + 2] = temp;
        }
        LOGCATE("DecodeBMP success ");
        return pixelData;
    }
    LOGCATE("DecodeBMP error ");
    return nullptr;
}

unsigned char *ReadBMP(const char *bmpPath, int &width, int &height) {
//    int nFileSize = 0;
    unsigned char *bmpFileContent = const_cast<unsigned char *>(LoadFileContent(bmpPath));
    if (bmpFileContent == NULL) {
        return 0;
    }
//    int bmpWidth = 0, bmpHeight = 0;
    unsigned char *pixelData = DecodeBMP(bmpFileContent, width, height);
    if (pixelData == NULL) {
        delete[] bmpFileContent;
        LOGCATE("CreateTextureFromBMP error ");
        return 0;
    }
//    GLuint texture = CreateTexture2D(pixelData, bmpWidth, bmpHeight, GL_RGB);
//    delete[] bmpFileContent;
//    LOGCATE("CreateTextureFromBMP success ");
//    return texture;
    return bmpFileContent;
}

#endif //C_NDKTEST_READFILEUTIL_H

native-lib.cpp

添加读图片的调用

#include <jni.h>
#include <string>

#include "demo/BgDemo.h"
#include "demo/TextureDemo.h"
#include "logUtil.h"
#include "ReadFileUtil.h"

//BgDemo *triangleDemo = NULL;
TextureDemo *triangleDemo = NULL;

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_c_1ndktest_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject clazz) {
    std::string hello = "Hello NativeImpl from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_c_1ndktest_NativeImpl_init(JNIEnv *env, jclass instance) {
    if (triangleDemo == NULL) {
//        triangleDemo = new BgDemo();
        triangleDemo = new TextureDemo();
    }
    // load shader
    triangleDemo->getShaderCode((char *) LoadFileContent("vertex.vs"), 0);
    triangleDemo->getShaderCode((char *) LoadFileContent("fragment.fs"), 1);
    int width = 0, height = 0;
    unsigned char *img = ReadBMP("f.bmp", width, height);
    triangleDemo->getTexturedata(img, width, height);
    triangleDemo->Init();
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_c_1ndktest_NativeImpl_OnSurfaceChanged(JNIEnv *env, jclass instance,jint width, jint height) {
    triangleDemo->OnSurfaceChanged(width,height);
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_c_1ndktest_NativeImpl_draw(JNIEnv *env, jclass instance) {
    triangleDemo->draw();
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_c_1ndktest_NativeImpl_stringFromJNI(JNIEnv *env, jclass clazz) {
    std::string hello = "Hello NativeImpl from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_c_1ndktest_NativeImpl_InitScene(JNIEnv *env, jclass clazz, jobject mrg) {
    g_mrg = AAssetManager_fromJava(env, mrg);
}

显示效果

在这里插入图片描述

入门教程就结束了 ~其实还有很多问题,比如坐标变换,还有翻转屏幕图片消失等问,compute shader学习等等,下期再说吧 ~

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
使用NDKOpenGL ES 3.0来绘制一个三角形可以分为以下几个步骤: 1. 首先,创建一个安卓项目,并配置NDK环境。 2. 在项目的jni目录下,创建一个C/C++源文件triangle.c。 3. 在triangle.c文件中,引入相关的头文件,包括<jni.h>和<GLES3/gl3.h>。 4. 在triangle.c文件中,实现一个JNI函数,用于绘制三角形。函数的参数为Surface对象。 5. 在JNI函数中,通过EGL和GLES初始化OpenGL环境,并创建一个EGLSurface用于后续的绘制操作。 6. 在JNI函数中,创建一个顶点数组和顶点缓冲,并将顶点数据存入顶点缓冲。 7. 在JNI函数中,编写着色器代码,包括顶点着色器和片段着色器,并编译和链接它们。 8. 在JNI函数中,通过glClearColor()函数设置清空屏幕时的颜色。 9. 在JNI函数中,通过glClear()函数清空屏幕,并启用深度测试。 10. 在JNI函数中,通过glViewport()函数设置视口大小。 11. 在JNI函数中,通过glUseProgram()函数使用着色器程序。 12. 在JNI函数中,通过glVertexAttribPointer()函数设置顶点数据的属性,并启用顶点属性。 13. 在JNI函数中,通过glDrawArrays()函数绘制三角形。 14. 在JNI函数中,通过eglSwapBuffers()函数交换绘制的缓冲区。 15. 在JNI函数中,清理OpenGL环境,并释放资源。 16. 在Java层的MainActivity中,通过JNI调用C/C++函数进行绘制。 以上是绘制一个三角形的大致步骤。具体的细节和代码实现可以参考相关的OpenGL ES 3.0和NDK的文档和示例代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值