安卓NDK保姆级OnpenGL绘制三角形(详细到爆炸)

前言

首先我们学习一个东西,都要先知道这个东西到底有什么用,有目的的去学习某个东西,才能更好的掌握它。

OpenGL是一个强大而灵活的图形库,为开发人员提供了广泛的工具和功能,以实现各种应用程序中的图形需求。

看起来这个描述好像有点抽象,其实从我本人的所闻所见来看,其实只要涉及图形的需求,都可以通过OpenGL实现。例如相机预览的绘制,美颜的实现,滤镜的实现等。当然啦,你还可以用它来做游戏,画正方形。

那我们在安卓平台使用OpenGL前需要掌握什么知识呢。

首先OpenGL提供了各种高级语言的api接口,争取做到了什么语言都能使用其进行实现需求,但是像Java的接口很少去使用,因为使用Java的话整体的效率都比较低,比较常见的是使用C++语言进行编写,在该帖子中我们也是在C++的基础上进行讲解。

其次我们还要学习一下简单的NDK编程,那什么是NDK呢?简单来说,就是我们可以在java代码调用C++代码。当前主流的安卓开发使用的语言还是Java,而后续我们绘制图形是使用的C++,因此使用NDK就至关重要了。

OpenGL基础知识

我在这里不会详细的去讲述OpenGL一些基础的概念,我比较建议学习OpenGL的同学先去看一下OpenGL的官方文档,超级无敌详细,我仔细看了一遍,对加深理解超级有用。

你好,三角形 - LearnOpenGL CN (learnopengl-cn.github.io)

这篇文章主要是从安卓开发如何使用OpenGL出发,尽可能做到详细。当然对于一些超级重要的基础知识,我也会在给出代码示例的同时讲述一下我对他们的理解,方便大家理解。

我现在会先列出三个超级重要的概念。

  • 顶点数组对象:Vertex Array Object,VAO
  • 顶点缓冲对象:Vertex Buffer Object,VBO
  • 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO

这三个专业名词在官方文档最开始就有介绍,我现在再提醒一遍,超级无敌重要,你们一定要对他们先有一个印象 ,后续我会结合代码给你们讲讲我对这三个专业名词的理解。

环境搭建

首先我新建一个项目,我们需要创建一个NDK项目,所幸AS现在提供了一个快速创建的方式——Native C++

构建好项目之后,会发现其实跟一般的安卓项目没有啥区别,最显著的区别就是多了一个cpp文件夹,我们后续绘制代码大部分都会在这个文件夹下实现。其中我们需要注意的是Cmakelist配置文件。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.

# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)

# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("stickersquareapplication")

# 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.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        native-lib.cpp)

# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log)

这是基础的配置文件的样子,我们对里面的每个部分进行解释

该CMakeLists.txt文件的内容如下:

  1. cmake_minimum_required(VERSION 3.22.1) 这一行指定了项目所需的最低CMake版本。

  2. project("stickersquareapplication") 这一行声明了项目的名称,你可以通过${PROJECT_NAME}${CMAKE_PROJECT_NAME}来引用项目名称。

  3. add_library(${CMAKE_PROJECT_NAME} SHARED native-lib.cpp) 这一行创建并命名一个库,并将相对路径为native-lib.cpp的源代码文件与该库关联起来。在这个示例中,库的名称与项目名称相同。

  4. target_link_libraries(${CMAKE_PROJECT_NAME} android log) 这一行指定了要链接到目标库的库文件。在这个示例中,目标库${CMAKE_PROJECT_NAME}将链接到androidlog库。

这个示例中的CMake配置用于构建一个名为"stickersquareapplication"的Android项目,其中包含一个名为"native-lib"的共享库。共享库的源代码位于native-lib.cpp文件中。链接库androidlog可用于支持Android应用程序开发,其中android库提供了与Android系统的接口,log库用于在Android的Logcat中输出日志。

简单来说,后续我们新建的C++文件,都要加到项目共享库,例如我们添加了一个triangle.cpp文件,我们需要在add_library中添加相对应的cpp文件

而我们后续需要用的第三方库,我们则需要写到target_link_libraries中,例如我们画三角形需要用到GLESv3库,则需要在target_link_libraries中添加GLESv3。

环境搭建好之后,我们就可以开始写代码了呀!

代码编写

为了方便大家理解,我们把代码编写分成了两部分,首先第一部分是应用层的编写,第二部分是Native绘画代码的编写。第一部分主要是使用Java构建一个简单的应用界面。第二部分使用C++进行绘画三角形。

界面应用部分

OpenGL为了方便开发者在安卓平台使用,提供了一个GLSurfaceView类作为OpenGL绘制出图形的显示控件。而我们要做的是自定义一个类继承GLSurfaceView类。而GLSurfaceView中还需要一个render,我们则需要自定义一个MyRender类进行

GLSurfaceViewRenderer 是用来管理和绘制 OpenGL 图形的接口。它提供了一种在 Android 应用程序中使用 OpenGL 进行图形渲染的方式。

GLSurfaceView 是 Android 提供的一个用于显示 OpenGL 图形的视图容器,它可以在 Android 应用程序中创建一个与 OpenGL 渲染上下文相关联的视图,并处理与 OpenGL 相关的生命周期和事件。但是,GLSurfaceView 并不直接进行图形渲染,而是依赖于一个实现了 Renderer 接口的类来完成实际的渲染工作。

Renderer 接口包含了三个主要的方法:

  • onSurfaceCreated():在第一次创建 GLSurfaceView 或设备被唤醒后,会调用该方法。在这个方法中,您可以执行 OpenGL 环境的初始化操作,如设置背景颜色、启用深度测试等。

  • onSurfaceChanged():在 GLSurfaceView 的大小发生变化时调用,比如屏幕旋转或窗口大小改变。您可以在这个方法中重新设置视口(viewport)和投影矩阵,以适应新的尺寸。

  • onDrawFrame():在每一帧需要绘制时调用。您可以在这个方法中执行实际的绘制操作,比如绘制图形、纹理贴图等。

通过实现 Renderer 接口,您可以控制 OpenGL 的渲染过程,并在每一帧中更新和绘制图形。您可以处理用户输入、更新模型数据、设置着色器程序、绑定纹理等操作,以实现您的自定义图形效果。

package com.example.stickersquareapplication;

import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;

public class MyGLSufaceView extends GLSurfaceView {
    public MyGLSufaceView(Context context) {
        super(context);
    }

    public MyGLSufaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setEGLContextClientVersion(3);
        MyRender myRender = new MyRender(context.getAssets());
        setRenderer(myRender);
    }
    
}
package com.example.stickersquareapplication;

import android.content.res.AssetManager;
import android.opengl.GLSurfaceView;

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

public class MyRender implements GLSurfaceView.Renderer {

    private AssetManager assetManager;
    public MyRender(AssetManager assetManager) {
        this.assetManager=assetManager;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    }

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

    }

    @Override
    public void onDrawFrame(GL10 gl) {

    }
}

MyGLSurfaceView 跟 MyRender的架子构造好之后,我们就可以将MyGLSurfaceView作为控件放到xml布局文件中。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <com.example.stickersquareapplication.MyGLSufaceView
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

自此,应用层的架子就建好了,我们接下来看一下Native C++部分,这才是重中之中。

Native C++

脚手架

不知道大家还记不记得在应用层中提到的Render,其中的三个方法都是干什么的可以读多几遍,我们Native C++部分将从这三个方法出发,其实单纯的使用Java的OpenGL接口的话,可以直接Render类的三个方法中使用OpenGl接口方法进行绘制。

而这篇帖子,我们使用NDK的方式,创建三个native方法,将这三个方法的执行转移到native C++,以下是代码示例。

package com.example.stickersquareapplication;

import android.content.res.AssetManager;
import android.opengl.GLSurfaceView;

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

public class MyRender implements GLSurfaceView.Renderer {

    private AssetManager assetManager;

    private native void onNdkSurfaceCreated();
    private native void onNdkSurfaceChanged(int width,int height);
    private native void onNdkSurfaceDrawFrame();

    public MyRender(AssetManager assetManager) {
        this.assetManager=assetManager;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        onNdkSurfaceCreated();
    }

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

    @Override
    public void onDrawFrame(GL10 gl) {
        onNdkSurfaceDrawFrame();
    }
}
#include <jni.h>
#include <string>
#include <GLES3/gl3.h>

extern "C" JNIEXPORT jstring

JNICALL
Java_com_example_stickersquareapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_stickersquareapplication_MyRender_onNdkSurfaceCreated(JNIEnv *env, jobject thiz) {
    // TODO: implement onNdkSurfaceCreated()
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_stickersquareapplication_MyRender_onNdkSurfaceChanged(JNIEnv *env, jobject thiz,jint width,jint height) {
    // TODO: implement onNdkSurfaceChanged()
    glViewport(0,0,width,height);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_stickersquareapplication_MyRender_onNdkSurfaceDrawFrame(JNIEnv *env,
                                                                         jobject thiz) {
    // TODO: implement onNdkSurfaceDrawFrame()
    //清空颜色缓存区以及深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);
}

到此,按照我的步骤来的话,就可以生成一个脚手架apk,我们后续就可以绘制背景颜色,绘制咱们第一个三角形了,脚手架apk的样子大概长下面这个样子。

 什么都没有,黑不溜秋的。但是万事开头难,我们已经成功了一大半了。接下来我会讲一下脚手架的几个核心的语句代表着什么意思。

首先看一下自定义Render类中的assetManager

public MyRender(AssetManager assetManager) {
        this.assetManager=assetManager;
    }

我们传递assetManager到MyRender中主要是因为后续我们需要编写顶点着色器glsl文件跟片段着色器 glsl文件,assetManager可以帮助我们拿到这两个文件。

其次还需要注意一下glViewport(0,0,width,height);,这个方法是定义显示的窗口多大,所以一般在SurfaceChanged的时候进行调用。

glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);这个方法主要是清空颜色缓存区以及深度缓冲区。如果不清空颜色缓冲区和深度缓冲区,则之前绘制的图像数据将保留在缓冲区中,新的绘制操作将在原有数据的基础上进行。

最后还有一个地方要注意的是,我们在这里使用到了GLES3的库,所以我们需要在cmakelist中添加GLES库的应用

target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        GLESv3)

头文件

我们为了可以让层次清晰,我们要绘制一个三角形,我们创建一个Triangle类,然后我们还需要一个实现绑定着色器程序的工具类。为了逻辑通畅,我先将这两个类的头文件进行讲解,让大家对大体有个理解。

Triangle.h
//
// Created by yangjunsheng on 2023/11/6.
//

#ifndef STICKERSQUAREAPPLICATION_TRIANGLE_H
#define STICKERSQUAREAPPLICATION_TRIANGLE_H

#include "GLES3/gl3.h"
#include "GLUtil.h"


class Triangle {
    GLuint mProgram;//着色器程序
    GLUtil gLuint;//自己写的工具类
    GLuint mVertexShader;//顶点着色器
    GLuint mFragmentShader;//片段着色器
    Triangle();
    ~Triangle();
};


#endif //STICKERSQUAREAPPLICATION_TRIANGLE_H

首先我们需要明确一点,在OpenGl中无论绘画什么,都需要顶点着色器,片段着色器,着色器程序。故此我们需要在Triangle类中声明这几个东西,同时我们需要知道一个是这里写的GLUtil是什么、有什么作用。着色器程序需要被创建,顶点着色器、片段着色器需要绑定到着色器程序上面,这时候GLUtil就是为了将这些操作封装起来,成为一个工具类,提高代码的效率。

GLUtil这个工具类写好之后,所有程序都可以使用它来提高工作效率,故此我们要做的就是理解GLUtil中每种方法,不需要死记硬背。

GLUtil.h
//
// Created by yangjunsheng on 2023/11/6.
//

#ifndef STICKERSQUAREAPPLICATION_GLUTIL_H
#define STICKERSQUAREAPPLICATION_GLUTIL_H

#include "GLES3/gl3.h"


class GLUtil {
public:
    //加载着色器
    GLuint LoadShader(GLenum shaderType, const char *pSource);

    //创建着色器程序
    GLuint CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource,
                         GLuint &vertexShaderHandle, GLuint &fragShaderHandle);

    //删除着色器程序
    void DeleteProgram(GLuint &program);
};


#endif //STICKERSQUAREAPPLICATION_GLUTIL_H

看头文件我们可以清晰的知道这个工具类的作用。

LoadShader方法主要适用于加载着色器,我们写好顶点着色器、片段着色器文件,他们都只是文件,里面的内容就相当于字符串,我们需要将这些字符串所表达的意思加载到着色器中,LoadShader中就需要实现这部分的逻辑

CreateProgram方法主要作用于创建一个着色器程序。我们加载好顶点着色器和片段着色器之后,我们需要创建一个着色器程序,然后将顶点着色器、片段着色器绑定到着色器程序中,CreateProgram中就需要实现这部分的逻辑。

DeleteProgram方法,就是摧毁掉着色器程序,C++不想Java,它是不会有GC这种东西,去自动回收不需要的空间,这需要我们手动的将内存空间释放。

cpp源文件

对所需的两个类的作用有个大概了解之后,我们来看看他们的具体源代码实现部分。

为了方便大家理解,我先讲述一下GLUtil的源代码部分

GLUtil.cpp
//
// Created by yangjunsheng on 2023/8/30.
//
#include "GLUtil.h"
#include "LogUtils.h"

GLuint GLUtil::LoadShader(GLenum shaderType, const char *pSource) {
    GLuint shader =0;
    //创建着色器,通过传进来的shaderType创建顶点着色器和片段着色器
    shader=glCreateShader(shaderType);
    if(shader){
        glShaderSource(shader,1,&pSource,NULL);
        //编译shader
        glCompileShader(shader);

        GLint compileStatus;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
        if (compileStatus != GL_TRUE) {
            GLint infoLogLength;
            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
            if (infoLogLength > 0) {
                char* infoLog = new char[infoLogLength];
                glGetShaderInfoLog(shader, infoLogLength, NULL, infoLog);
                LogUtils::error("yjs", infoLog);
                delete[] infoLog;
            }
            glDeleteShader(shader);
            shader = 0;
        }
    }
    return shader;
}

GLuint GLUtil::CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource,
                             GLuint &vertexShaderHandle, GLuint &fragShaderHandle) {
    GLuint program =0;
    vertexShaderHandle=LoadShader(GL_VERTEX_SHADER,pVertexShaderSource);
    if (!vertexShaderHandle){
        return program;
    }
    fragShaderHandle=LoadShader(GL_FRAGMENT_SHADER,pFragShaderSource);
    if (!fragShaderHandle){
        return program;
    }
    program=glCreateProgram();
    if (program){
        LogUtils::debug("yjs","create program success");
        //将着色器附加到着色器程序中
        glAttachShader(program,vertexShaderHandle);
        glAttachShader(program,fragShaderHandle);
        glLinkProgram(program);

        GLint linkStatus;
        glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
        if (linkStatus != GL_TRUE) {
            GLint infoLogLength;
            glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
            if (infoLogLength > 0) {
                char* infoLog = new char[infoLogLength];
                glGetProgramInfoLog(program, infoLogLength, NULL, infoLog);
                LogUtils::error("yjs", infoLog);
                delete[] infoLog;
            }
            glDeleteProgram(program);
            program = 0;
        }

        //链接好之后就可以把之前的shader给删掉了
        glDetachShader(program,vertexShaderHandle);
        glDeleteShader(vertexShaderHandle);
        vertexShaderHandle=0;
        glDetachShader(program,fragShaderHandle);
        glDeleteShader(fragShaderHandle);
        fragShaderHandle=0;

    }
    return program;
}

void GLUtil::DeleteProgram(GLuint &program) {
    if (program){
        glUseProgram(0);
        glDeleteProgram(program);
        program=0;
    }
}

这个类作为工具类,复用性是很强的,任何相关的项目都可以将该工具类移植到里面,可以直接使用。

我这边对其中一些核心语句进行讲解一下。我讲解的基础都是认为你是有一定的基础知识的情况下进行讲解,不会讲的特别细,所以看这篇文章之前,一定要对基础知识有一个印象。

首先来看一下源代码中第一个方法LoadShader,看方法名我们就可以知道,这个方法时实现了着色器的加载,我们前面说了,我们编写的glsl文件,不加载的话,对于整个程序来说,编写的glsl文件就是一堆字符。LoadShader(GLenum shaderType, const char *pSource)存在两个形参,第一个就是传入加载的类型,判断是顶点着色器还是片段着色器。第二个参数就是我刚刚说的我们编写的glsl文件里面的字符内容。对于glsl文件我们获取它的流程大概是,编写好文件后,通过assetmanager获取到文件,然后通过IO流将里面的内容已字符串的形式获取出来,最后就传到这个方法进行加载着色器。

这个方法会在createProgram中进行调用,毕竟加载着色器就是为了将着色器绑定到着色器程序中。

接下来看看这个方法的核心语句

//获取shader资源
glShaderSource(shader,1,&pSource,NULL);
//编译shader
glCompileShader(shader);

加载着色器的实际语句就这两句,那个方法下面的语句都是为了代码健壮性写的,主要是判断是否加载成功。

在本例子中,应该会使用到loadshader方法两次,分别是顶点着色器和片段着色器的加载。

讲解完加载着色器方法,我们再来看看这个工具类第二个方法,创建着色器程序方法CreateProgram。加载好顶点着色器、片段着色器之后,我们需要有一个程序去控制这两个着色器怎么绘制,这时候创建着色器程序就非常重要。我们先来看看他的几个重要语句。

//方法声明 

CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource,
                             GLuint &vertexShaderHandle, GLuint &fragShaderHandle)

//调用加载顶点着色器
vertexShaderHandle=LoadShader(GL_VERTEX_SHADER,pVertexShaderSource);
//调用加载片段着色器
fragShaderHandle=LoadShader(GL_FRAGMENT_SHADER,pFragShaderSource);
//创建着色器程序
program=glCreateProgram();
//将着色器附加到着色器程序中
glAttachShader(program,vertexShaderHandle);
glAttachShader(program,fragShaderHandle);
glLinkProgram(program);
//链接好之后就可以把之前的shader给删掉了
glDetachShader(program,vertexShaderHandle);
glDeleteShader(vertexShaderHandle);
vertexShaderHandle=0;
glDetachShader(program,fragShaderHandle);
glDeleteShader(fragShaderHandle);
fragShaderHandle=0;

上面就是创建着色器整个流程,首先我们来看看这个方法传进来的四个形参分别代表什么,前面两个字符形参分别是顶点着色器和片段着色器glsl文件中的字符内容,用来作为loadShader方法的形参用的。后面两个是两个着色器的变量名,加载好着色器之后将着色器赋值给这两个变量。

其次就是调用loadShader方法去加载着色器,加载好之后进行赋值。

接着就是通过glCreateProgram创建一个着色器程序出来,但是值得注意的是,这时候的程序没有绑定任何着色器,故此需要后面的操作。

glAttachShader方法是将两个着色器附加到着色器程序中。

glLinkProgram方法是于将它们着色器链接为一个完整的着色器程序。

接着就是链接好之后,我们可以把之前的shader给删除掉了,至于为什么要删掉呢,在OpenGL中,一般建议在链接着色器程序之后删除掉原来的顶点着色器和片段着色器。这样做的主要原因是为了释放资源和提高内存利用效率。

当着色器程序链接成功后,顶点着色器和片段着色器的对象就不再需要了,它们的内存可以被释放掉。这样可以避免在程序运行的过程中浪费内存,并且有助于提高程序的性能。另外,删除不再需要的着色器对象也有助于减少内存泄漏的可能性,保持程序的稳定性。

那这时候就有些人就会有疑问了,按照这个说法,是不是着色器程序中已经将所需要的着色器给拷贝进去了呢?

实际上,当你调用 glLinkProgram 链接着色器程序成功后,OpenGL内部并没有将顶点着色器和片段着色器的代码拷贝进着色器程序中。删除着色器对象并不会影响着色器程序的完整性。

删除着色器对象只是告诉OpenGL你不再需要它们了,这样OpenGL就可以释放它们占用的内存。着色器程序本身仍然包含了链接时所需的信息,并且可以继续在渲染过程中使用。

因此,删除着色器对象只是为了清理不再需要的资源,而不是将它们的内容拷贝到着色器程序中。着色器程序已经包含了链接时所需的所有信息,可以独立于原始的着色器对象存在。

最后就是DeleteProgram方法,可能经常使用Java开发的开发者一直都很难习惯去及时释放控件,那是因为java有一个JVM可以进行GC(垃圾回收机制),故此java工作者更应该注意控件的释放。

glUseProgram(0);
glDeleteProgram(program);
program=0;

Triangle.cpp 

我们看完了GLUtil工具类,我们来看看重头戏,三角形类,之所以先讲工具类是因为在三角形类中有大量使用GLUtil工具类的方法。接下来我们先整体看一下该类的所有方法。

//
// Created by yangjunsheng on 2023/8/24.
//

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

Triangle::Triangle() {

}

Triangle::~Triangle() {
    if (mProgram){
        
        //删除program
        gLuint.DeleteProgram(mProgram);
    }
}

void
Triangle::init(AAssetManager *pManger, const char *vShaderFileName, const char *fShaderFileName) {
    //通过获取着色器代码文件获取到里面的代码,将其转换成字符串的形式
    AAsset *vFile = AAssetManager_open(pManger, vShaderFileName, AASSET_MODE_BUFFER);
    off_t vFileLength = AAsset_getLength(vFile);
    char *vContentBuff = (char*)malloc(vFileLength);
    AAsset_read(vFile,vContentBuff,vFileLength);
    //千万要记住关闭文件流
    AAsset_close(vFile);
    
    //接下来获取片段着色器代码转换成字符串
    AAsset *fFile = AAssetManager_open(pManger, fShaderFileName, AASSET_MODE_BUFFER);
    off_t fFileLength = AAsset_getLength(fFile);
    char *fContentBuff = (char*)malloc(fFileLength);
    AAsset_read(fFile,fContentBuff,fFileLength);
    AAsset_close(fFile);

    mProgram=gLuint.CreateProgram(vContentBuff,fContentBuff,mVertexShader,mFragmentShader);

    free(vContentBuff);
    free(fContentBuff);
}

void Triangle:: Draw() {
    if(mProgram==0){
        return;
    }
    //将要输入的顶点坐标数据

    GLfloat vVertices1[]={
            0.0f,0.5f,0.0f,
            -0.5f,-0.5f,0.0f,
            0.5f,-0.5f,0.0f
    };
    GLfloat vVertices2[]={
            0.0f,0.8f,0.0f,
            -0.3f,0.5f,0.0f,
            0.3f,0.5f,0.0f
    };
    //+创建并绑定VA0 VBO
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER,VBO);
    //-创建并绑定VA0 VBO

    glBufferData(GL_ARRAY_BUFFER, sizeof(vVertices1), vVertices1, GL_STATIC_DRAW);
    //设置顶点属性
    // 启用顶点属性并设置指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0);
    glEnableVertexAttribArray(0);
    //解绑VAO、VBO
    /*如果我们忘记解绑VBO或VAO,可能会导致意外的结果。
     * 例如,如果我们在解绑VBO之前绑定了另一个VBO并设置了不同的顶点数据,
     * 那么后续的绘制操作将使用新绑定的VBO的数据而不是预期的数据。
     * 通过显式解绑状态,我们可以确保每个对象在适当的时候进行更改。*/
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    unsigned int VBO1, VAO1;
    glGenVertexArrays(1, &VAO1);
    glGenBuffers(1, &VBO1);

    glBindVertexArray(VAO1);
    glBindBuffer(GL_ARRAY_BUFFER,VBO1);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vVertices2), vVertices2, GL_STATIC_DRAW);
    //设置顶点属性
    // 启用顶点属性并设置指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    glUseProgram(mProgram);
    // 更新uniform颜色
    float timeValue = second++;
    float greenValue = sin(timeValue) / 2.0f + 0.5f;
    int vertexColorLocation = glGetUniformLocation(mProgram, "ourColor");
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);




    /*应该使用VA0*/
    //绘制三角形
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES,0,3);
    glBindVertexArray(0);
    glBindVertexArray(VAO1);
    glDrawArrays(GL_TRIANGLES,0,3);
    glBindVertexArray(0);

    // 清理资源
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO1);
    glDeleteBuffers(1, &VBO1);


}

首先我们来看看init方法,这个方法主要是实现了获取着色器glsl文件内容,然后传进工具类的CreateProgram方法,进行创建着色器程序。看见没工具类封装的好处就出来了!我们来看看核心语句。

首先是打开文件进行IO流的操作,这部分其实会使用就行,我个人一直觉得IO流这个东西没必要记得怎么手撕,需要用的时候可以及时找到就行。大家要是怕找不到,可以直接收藏我这篇文档,那可就一下就能找到了。

然后接下来获取到glsl文件的内容并转换成字符串之后,就可以调用工具类的createProgram进行创建program,自此init方法就执行完成,是不是有之前的铺垫就简单多了。

讲述完init方法之后,我们就可以来到这篇文档的重头戏,三角形的绘制方法!Draw,我们首先来想一下,经过前面的init方法,我们现在有了什么,顶点着色器和片段着色器已经链接到着色器程序中,程序又创建好了,现在相当于我们的工具都已经准备好,我们现在需要的就是在哪画,怎么画了是吧,draw方法要实现的就是这个。

最后重申一次,这篇帖子的编写基础是你们已经把我在最上面的网站认真看了一遍,有一个基本概念,要是没有基本概念,你们估计看我这篇文章云里雾里的,快去看,快去看!

我上面的代码是画了两个三角形,我后面的讲解只会讲其中的一个,因为在这段代码中,两个三角形绘制的方式都是一样,都是一个VAO,跟一个VBO(这两个东西,前面有叫你们去看噢,忘记再去温习理解一下)。

首先我们看一下,我们需要定义我们三角形的三个坐标

//这个很明显是一个等腰三角形
GLfloat vVertices1[]={
        0.0f,0.5f,0.0f,
        -0.5f,-0.5f,0.0f,
        0.5f,-0.5f,0.0f
};

紧接着,我们需要创建VAO\VBO

//创建VAO
glGenVertexArrays(1, &VAO);
//创建VBO
glGenBuffers(1, &VBO);

 glGenVertexArrays 函数用于生成顶点数组对象(Vertex Array Object, VAO)。函数的第一个参数是需要生成的VAO对象的数量,而第二个参数则是用来存储生成的VAO对象名称的数组的地址。glGenBuffers也是一样,唯一的区别就是这个方法是拿来创建VBO的。

可能大家伙对VAO跟VBO理解的有点吃力,我在这说说我对他们的理解:

VBO:负责存储顶点、颜色等一些顶点数据。

VAO:负责存储顶点属性的配置位置。

简单来说,VBO就是单纯的存储东西,没有任何操作,而VAO就是对存储数据的配置,包括VBO的绑定和设置顶点属性指针的方式。

同个VBO可以根据不同的VAO呈现不一样的效果,当绘制不同的物体活切换不同的顶点格式时,使用VAO就可以十分方便了。

*VAO在场景中存在多个物体,每个物体具有不同的顶点属性的时候特别好用。

我们通过一个例子理解VAO的存储顶点格式,例如我们打算把颜色数据加进顶点数据。

float vertices[]={

//位置              //颜色

0.5f,-0.5f,0.0f, 1.0f,0.0f,0.0f,

-0.5f,-0.5f,0.0f,0.0f,1.0f,0.0f,

0.0f,0.5f,0.0f, 0.0f,0.0f,1.0f

}

看见没。现在我的顶点数据不只有位置数据。这时候必须重新配置顶点属性指针(VAO配置一个顶点属性),计算机不知道你哪些是顶点坐标,哪些是颜色坐标,配置顶点属性指针简单理解就是告诉计算机哪个坐标是干什么的,已一个什么顺序画之类的。

回到示例,对应到代码中就是

//绑定VAO、VBO
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);

//绑定顶点数据到VBO中
glBufferData(GL_ARRAY_BUFFER, sizeof(vVertices1), vVertices1, GL_STATIC_DRAW);
//设置顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0);
// 启用顶点属性并设置指针
glEnableVertexAttribArray(0);

我们这里着重讲一下最后这三句逻辑,每个方法的形参分别代表什么:

glBufferData第一个形参是代表写入的顶点数据的类型,第二个参数是顶点数据的大小,第三个参数顶点数据的内容,第四个参数可以理解成用处。

glVertexAttribPointer第一个参数是顶点的位置position,这个position是在我们写的顶点着色器glsl文件中有写,我们现在在这插个眼,记住我们设置的position是0,第二个参数是一个顶点所有数据的个数,第三个参数是每个顶点数据的数据类型,第四个是是否将坐标转换成标准的【-1,1】坐标系(不知道这个的快去看网站!),第五个参数是一个顶点占有的总的字节数,代码这里为三个float,所以是sizeof(float)*3,第六个是偏移量,当前指针指向的vertex内部的偏离字节数,可以唯一的标识顶点某个属性的偏移量,因为原本的代码顶点数据只有顶点坐标,所以我们只需要在这写上0,但是如果按照我们刚刚举例子的顶点数据来说,既有坐标又有颜色,这里就要有一个偏移量,移动多少才能接着去到位置坐标而不是颜色信息。

glEnableVertexAttribArray的唯一一个参数就是启用顶点的index,这里对应glVertexAttribPointer的第一个参数。

在绑定好VAO跟VBO之后,我这边建议对VAO跟VBO进行一个解绑,我在示例代码也有体现,这样子能更好的内存管理,不用的时候解绑,避免内存泄漏。

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

VAO跟VBO都设置好之后, 我们就可以使用之前创建好的着色器程序开始画画啦,还记得之前说的吗,我创建好着色器程序相当于创建好工具,但是不知道应该怎么画、画什么,设置VAO跟VBO就相当于指定了怎么画、画什么。

//调用着色器程序
glUseProgram(mProgram);


// 更新uniform颜色
float timeValue = second++;
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(mProgram, "ourColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);




/*应该使用VA0*/
//绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES,0,3);
glBindVertexArray(0);

在我们调用了着色器程序之后 ,看到一个更新颜色的逻辑,这是因为我这个程序设计颜色信息并不是通过顶点数据写入,这个我先不讲,等到后面讲到glsl文件一并给你说清楚。

我们先来看后面,在绘制前,绑定刚刚设置好的VAO,这时候VAO已经有VBO的数据链接,以及顶点属性(怎么通过顶点数据绘制)。随后调用glDrawArrays进行绘制三角形,绘制完之后,还是一样的解绑。

自此cpp源文件的部分就讲解完了,我们接下来看看glsl文件,也就是我们的顶点着色器跟片段着色器怎么去写

GLSL文件

在这之前,请确保你看了下面的这篇官方文档

着色器 - LearnOpenGL CN (learnopengl-cn.github.io)

首先看看创建,我们在AS中为了开发方便,可以下载一个glsl插件,这样就可以快捷创建glsl文件

首先我们要创建一个asset文件,放静态资源

顶点着色器 
#version 300 es

layout(location = 0) in vec4 vPosition;
void main() {
    gl_Position=vPosition;
}

接下来咱们来仔细看看这几行代码,别看它内容少,蕴含的知识点还不少呢。

首先第一行是代表使用的GL的版本,我们现在一般都是3.0。

第二句是代表顶一个输入变量 ,看到这个location=0是不是很眼熟,这句话表示这个输入位置的内存位置为0。我们来看看刚刚的那个语句

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void*)0);

这个方法的第一个参数就是指的就是我们在顶点着色器中定义的location,一下子是不是清晰了很多,你们一定想过,如果有很多顶点数据,那怎么确定将其跟着色器对应上呢,答案就是这个locaton 。

现在重新来看这个语句,意思是不是就是我要把在C++中定义的顶点坐标数据,定义到这个输入位置变量中。

片段着色器
#version 300 es

precision mediump float;
out vec4 fragColor;

uniform vec4 ourColor;

void main() {
    fragColor = ourColor;
}

我们结合刚刚红色重点部分

// 更新uniform颜色

float timeValue = second++;

float greenValue = sin(timeValue) / 2.0f + 0.5f;

int vertexColorLocation = glGetUniformLocation(mProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

我们就可以知道了,我们在C++中可以获取到ourColor并赋值,然后这个值就会通过片段着色器的方式绘制上去。

再重复一次,我现在讲的所有都是需要你看完我前面提到的两个网址,没有看的,先去看,一遍看不懂,看多几遍,看再多别人的帖子,还不如看懂官方的文档。 

回到脚手架

triangle类整体写好之后,我们就可以在脚手架上引用我们的三角形类直接绘制啦

#include <jni.h>
#include <string>
#include "GLES3/gl3.h"
#include "Triangle.h"
#include <iostream>
#include "LogUtils.h"


Triangle triangle;
extern "C" JNIEXPORT jstring  JNICALL
Java_com_example_miniopenglapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_miniopenglapplication_MyRender_ndkOnDrawFrame(JNIEnv *env, jobject thiz) {
    // TODO: implement ndkOnDrawFrame()
    //清空颜色缓存区以及深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);
    LogUtils::debug("yjs","draw");
    triangle.Draw();
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_miniopenglapplication_MyRender_ndkOnSurfaceCreated(JNIEnv *env, jobject thiz,jobject assetManager) {
    // TODO: implement ndkOnSurfaceCreated()
    //清空颜色(以红色进行清空)
    glClearColor(1.0,1.0,0,1.0);
    //需要将assetManager转换为ndk可用
    AAssetManager *astManager = AAssetManager_fromJava(env, assetManager);
    if (NULL!=astManager){
        LogUtils::debug("yjs","astManager is not null");
        triangle.init(astManager,"vShader.vert","fShader.frag");
    }

}


extern "C"
JNIEXPORT void JNICALL
Java_com_example_miniopenglapplication_MyRender_ndkOnSurfaceChanged(JNIEnv *env, jobject thiz,
                                                                    jint width, jint height) {
    // TODO: implement ndkOnSurfaceChanged()
    //设置窗口大小
    glViewport(0,0,width,height);

}

总结

自此所有流程跟重要代码都讲解完了,我知道有些人还是会有点云里雾里的,我编写文档能力不好,写这个帖子,也主要是为了自己看懂,自己备份,我一会会直接把所有代码上传给大家学习,大家自己慢慢看,有不懂的可以私信笔者,我看到就会回。谢谢!

效果展示

我将给大家展示我的代码最后呈现出来一个什么样的效果

opengl绘制三角形

源代码

我会已附件的形式上传。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用NDK和OpenGL 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的文档和示例代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值