目录
- 一、OpenGL ES 3.0 示例效果:三角形
- 二、Native实现三角形
-
- 2.0 准备步骤
-
- 2.0.1 在AndroidManifest.xml中声明 OpenGL 要求
- 2.0.2 build.gradle中minSdkVersion声明超过18。
- 2.0.3 CMakeLists.txt里面的target_link_libraries,得添加GLESv三库,不要写GLESv2库了。
- 2.1 为 OpenGL ES 图形创建 Activity
-
- 2.1.1 编写HelloTriangleActivity
- 2.1.2 注册HelloTriangleActivity
- 2.2 构建渲染程序类HelloTriangleNativeRenderer
- 2.3 编写 NativeTriangle.h
- 2.4 编写NativeTriangle.cpp
- 2.5 关于 GLUtils.h
- 三、解释程序
-
- 3.1 创建简单的顶点和片段着色器
- 3.2 着色器的编译和加载
- 3.3 创建程序对象并链接颜色器
- 3.4 设置视口,去除颜色缓冲区
-
- 3.4.1 设置视口
- 3.4.2 去除颜色缓冲区
- 3.5 加载几何和绘制图元
-
- 3.5.1 加载几何形状
- 3.5.1 绘制图元
- 3.6 为什么不等边三角形呢?
- 四、总结
- 五、纯java实现
-
- 5.1 顶点着色器和片段着色器
- 5.2 编写Activity
- 5.3 编写GLSurfaceView.Renderer的实现HelloTriangleRenderer
- 5.4 编写 ESShader工具类
- 5.5 Java 和 C Native方式对比
- 六、源代码
为了介绍OpenGL ES 3.我们从一个简单的例子开始。
这个例子表明创建一个三角形OpenGL ES 3.0程序所需的步骤。
一、OpenGL ES 3.0 示例效果:三角形
效果如下:我们画了一个红色的三角形。
二、Native实现三角形
2.0 准备步骤
参考我以前的博客 【我的OpenGL解决学习高级之旅的问题NDK使用OpenGL ES 3.0 的api报错:error: undefined reference to ‘glUnmapBuffer‘
2.0.1 在AndroidManifest.xml中声明 OpenGL 要求
若使用您的应用程序 OpenGL 功能不一定可用于所有设备,所以你必须在那里 AndroidManifest.xml 这些要求包含在文件中。
如下所示,我们的声明版本是0x00030000
,表明需要支持 OpenGL ES 3.0
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
2.0.2 build.gradle中minSdkVersion声明超过18。
下面是我的build.gradle文件,声明minSdkVersion 为18
plugins {
id 'com.android.application' id 'kotlin-android' } android {
compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig {
applicationId "com.oyp.openglesdemo" minSdkVersion 18 targetSdkVersion 31 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild {
cmake {
cppFlags "-std=c 11"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
2.0.3 CMakeLists.txt里面的target_link_libraries,得添加GLESv3库,不要写成GLESv2库了。
参考我之前的博客:
【我的Android进阶之旅】NDK开发之CMake自定义搜索规则,减少每次都需要配置.cpp和.h的工作量
我们添加了GLESv3 库,并且创建了自定义搜索规则,来保护我们要编写的.cpp和.h文件
# 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.10.2)
# Declares and names the project.
project("openglesdemo")
# 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.
#设置包含的目录
include_directories(
glesdemo/triangle
glesdemo/lesson
glesdemo/lesson/lesson7
glesdemo/texture
glesdemo/triangle
utils
utils/graphics
utils/log
utils/time
)
#自定义搜索规则
file(GLOB src-files
${CMAKE_SOURCE_DIR}/*.cpp
${CMAKE_SOURCE_DIR}/*.h
${CMAKE_SOURCE_DIR}/*/*.cpp
${CMAKE_SOURCE_DIR}/*/*.h
${CMAKE_SOURCE_DIR}/*/*/*.cpp
${CMAKE_SOURCE_DIR}/*/*/*.h
${CMAKE_SOURCE_DIR}/*/*/*/*.cpp
${CMAKE_SOURCE_DIR}/*/*/*/*.h
)
add_library( # Sets the name of the library.
opengles-lesson-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${src-files}
)
# 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.
opengles-lesson-lib
EGL
# GLESv2 # 把opengl库文件添加进来,GLESv2
GLESv3 # 把opengl库文件添加进来,GLESv3
android
# Links the target library to the log library
# included in the NDK.
${log-lib})
2.1 为 OpenGL ES 图形创建 Activity
使用 OpenGL ES 的 Android 应用的 Activity 与其他任何具有界面的应用相同。
这种应用与其他应用的主要区别在于您在 Activity 的布局中添加了哪些内容。
在很多应用中,您可以使用 TextView
、Button
和 ListView
; 而在使用 OpenGL ES
的应用中,您还可以添加一个 GLSurfaceView
。
2.1.1 编写HelloTriangleActivity
我们编写一个 HelloTriangleActivity ,如下所示:
package com.oyp.openglesdemo.triangle
import android.opengl.GLSurfaceView
import com.oyp.openglesdemo.BaseGLActivity
class HelloTriangleActivity : BaseGLActivity() {
override fun getSurfaceView(): GLSurfaceView {
return GLSurfaceView(this)
}
override fun getRender(): GLSurfaceView.Renderer {
return HelloTriangleNativeRenderer()
}
}
HelloTriangleActivity
继承自 BaseGLActivity
,BaseGLActivity
是我们写的一个简单的封装,如下所示:
HelloTriangleActivity
重写了getSurfaceView()
和getRender()
两个抽象方法实现。
BaseGLActivity
定义了getSurfaceView()
和getRender()
两个抽象方法,并且判断了是否支持OpenGL ES 3.0,然后在onResume()和onPause()
方法上,调用了GLSurfaceView对应的方法。
package com.oyp.openglesdemo;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.pm.ConfigurationInfo;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
public abstract class BaseGLActivity extends Activity {
/** * Hold a reference to our GLSurfaceView */
protected GLSurfaceView mGLSurfaceView;
protected GLSurfaceView.Renderer renderer;
protected abstract GLSurfaceView.Renderer getRender();
protected abstract GLSurfaceView getSurfaceView();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
renderer = getRender();
mGLSurfaceView = getSurfaceView();
if(detectOpenGLES30()){
// Tell the surface view we want to create an OpenGL ES 3.0-compatible
// context, and set an OpenGL ES 3.0-compatible renderer.
int CONTEXT_CLIENT_VERSION = 3;
mGLSurfaceView.setEGLContextClientVersion(CONTEXT_CLIENT_VERSION);
mGLSurfaceView.setRenderer(renderer);
} else {
Log.e("HelloTriangle", "OpenGL ES 3.0 not supported on device. Exiting...");
return;
}
setContentView(mGLSurfaceView);
}
private Boolean detectOpenGLES30(){
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
ConfigurationInfo info = am.getDeviceConfigurationInfo();
return info.reqGlEsVersion >= 0x30000;
}
@Override
protected void onResume() {
// The activity must call the GL surface view's onResume() on activity onResume().
super.onResume();
mGLSurfaceView.onResume();
}
@Override
protected void onPause() {
// The activity must call the GL surface view's onPause() on activity onPause().
super.onPause();
mGLSurfaceView.onPause();
}
}
GLSurfaceView
是一种专用视图,您可以在其中绘制 OpenGL ES
图形。它本身并没有很大的作用。 对象的实际绘制由您在此视图中设置的 GLSurfaceView.Renderer
控制。
2.1.2 注册HelloTriangleActivity
在AndroidManifest.xml中注册该Activity
<activity android:name=".triangle.HelloTriangleActivity" android:label="@string/lesson_one"/>
2.2 构建渲染程序类HelloTriangleNativeRenderer
HelloTriangleNativeRenderer 继承自 GLSurfaceView.Renderer,我们重写了
onSurfaceCreated(gl: GL10, config: EGLConfig)
方法, 调用一次以设置视图的 OpenGL ES 环境。onSurfaceChanged(gl: GL10, width: Int, height: Int)
方法, 当视图的几何图形发生变化(例如当设备的屏幕方向发生变化)时调用。onDrawFrame(gl: GL10)
方法, 每次重新绘制视图时调用。
重写的内容是调用我们自己写的native方法
nativeSurfaceCreate()
nativeSurfaceChange(width: Int, height: Int)
nativeDrawFrame()
package com.oyp.openglesdemo.triangle
import android.opengl.GLSurfaceView
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
class HelloTriangleNativeRenderer : GLSurfaceView.Renderer {
external fun nativeSurfaceCreate()
external fun nativeSurfaceChange(width: Int, height: Int)
external fun nativeDrawFrame()
init {
System.loadLibrary("opengles-lesson-lib")
}
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
nativeSurfaceCreate()
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
nativeSurfaceChange(width, height)
}
override fun onDrawFrame(gl: GL10) {
nativeDrawFrame()
}
}
接下来我们得开始编写native的具体实现,也就是要用C++来编写代码了。
2.3 编写 NativeTriangle.h
NativeTriangle.h如下所示:
其中GLUtils.h是我们自己写的一个GL相关的工具类,我们后面再介绍
#pragma once
#include "../../utils/GLUtils.h"
namespace NAMESPACE_NativeTriangle {
class NativeTriangle {
public:
NativeTriangle();
~NativeTriangle();
void create();
void change(int width, int height);
void draw();
private:
GLuint mProgram;
int mWidth;
int mHeight;
};
}
2.4 编写NativeTriangle.cpp
在NativeTriangle.cpp中,我们实现了NativeTriangle.h定义的方法,并且使用JNI来实现了之前在java层的HelloTriangleNativeRenderer中调用的native方法。
#include "NativeTriangle.h"
// 可以参考这篇讲解: https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
namespace NAMESPACE_NativeTriangle {
// 顶点着色器
const char* VERTEX_SHADER_TRIANGLE =
"#version 300 es \n"
"layout(location = 0) in vec4 vPosition; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
"} \n";
// 片段着色器
const char* FRAGMENT_SHADER_TRIANGLE =
"#version 300 es \n"
"precision mediump float; \n"
"out vec4 fragColor; \n"
"void main() \n"
"{ \n"
" fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 ); \n"
"} \n";
// 我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)
// 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。
// 我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。
// https://learnopengl-cn.github.io/img/01/04/ndc.png
// https://developer.android.com/guide/topics/graphics/opengl#kotlin
// 在 OpenGL 中,形状的面是由三维空间中的三个或更多点定义的表面。
// 一个包含三个或更多三维点(在 OpenGL 中被称为顶点)的集合具有一个正面和一个背面。
// 如何知道哪一面为正面,哪一面为背面呢?这个问题问得好!答案与环绕(即您定义形状的点的方向)有关。
// 查看图片 : https://developer.android.com/images/opengl/ccw-winding.png
// 或者查看本地图片:Android_Java/Chapter_2/Hello_Triangle/ccw-winding.png
// 在此示例中,三角形的点按照使它们沿逆时针方向绘制的顺序定义。
// 这些坐标的绘制顺序定义了该形状的环绕方向。默认情况下,在 OpenGL 中,沿逆时针方向绘制的面为正面。
// 因此您看到的是该形状的正面(根据 OpenGL 解释),而另一面是背面。
//
// 知道形状的哪一面为正面为何如此重要呢?
// 答案与 OpenGL 的“面剔除”这一常用功能有关。
// 面剔除是 OpenGL 环境的一个选项,它允许渲染管道忽略(不计算或不绘制)形状的背面,从而节省时间和内存并缩短处理周期:
GLfloat vVertices[] = {
// 逆时针 三个顶点
0.0f, 0.5f, 0.0f, // 上角
-0.5f, -0.5f, 0.0f, // 左下角
0.5f, -0.5f, 0.0f // 右下角
};
NativeTriangle::NativeTriangle() {
}
NativeTriangle::~NativeTriangle() {
}
void NativeTriangle::create() {
GLUtils::printGLString("Version", GL_VERSION);
GLUtils::printGLString("Vendor", GL_VENDOR);
GLUtils::printGLString("Renderer", GL_RENDERER);
GLUtils::printGLString("Extensions", GL_EXTENSIONS);
mProgram = GLUtils::createProgram(&VERTEX_SHADER_TRIANGLE, &FRAGMENT_SHADER_TRIANGLE);
if (!mProgram) {
LOGD("Could not create program");
return;
}
// 设置清除颜色
glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
}
void NativeTriangle::draw() {
// Clear the color buffer
// 清除屏幕
// 在OpenGL ES中,绘图中涉及多种缓冲区类型:颜色、深度、模板。
// 这个例子,绘制三角形,只向颜色缓冲区中绘制图形。在每个帧的开始,我们用glClear函数清除颜色缓冲区
// 缓冲区将用glClearColor指定的颜色清除。
// 这个例子,我们调用了GLES30.glClearColor(1.0f, 1.0f, 1.0f, 0.0f); 因此屏幕清为白色。
// 清除颜色应该由应用程序在调用颜色缓冲区的glClear之前设置。
glClear(GL_COLOR_BUFFER_BIT);
// Use the program object
// 在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
// 当我们渲染一个物体时要使用着色器程序 , 将其设置为活动程序。这样就可以开始渲染了
glUseProgram(mProgram);
// Load the vertex data
// 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,
// 它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
// 所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
// 我们的顶点缓冲数据会被解析为下面这样子:https://learnopengl-cn.github.io/img/01/04/vertex_attribute_pointer.png
// . 位置数据被储存为32位(4字节)浮点值。
// . 每个位置包含3个这样的值。
// . 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
// . 数据中第一个值在缓冲开始的位置。
// 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
// Load the vertex data
// 第一个参数指定我们要配置的顶点属性。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
// 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
// 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
// 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
// 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。我们设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。
// 一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,
// (译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
// 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vVertices);
// 现在我们已经定义了OpenGL该如何解释顶点数据,
// 我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。
glEnableVertexAttribArray(0);
// glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。
// 第二个参数指定了顶点数组的起始索引,我们这里填0。
// 最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
// public static final int GL_POINTS = 0x0000;
// public static final int GL_LINES = 0x0001;
// public static final int GL_LINE_LOOP = 0x0002;
// public static final int GL_LINE_STRIP = 0x0003;
// public static final int GL_TRIANGLES = 0x0004;
// public static final int GL_TRIANGLE_STRIP = 0x0005;
// public static final int GL_TRIANGLE_FAN = 0x0006;
glDrawArrays(GL_TRIANGLES, 0, 3);
// 禁用 通用顶点属性数组
glDisableVertexAttribArray(0);
}
void NativeTriangle::change(int width, int height) {
mWidth = width;
mHeight = height;
LOGD("change() width = %d , height = %d\n", width, height);
// Set the viewport
// 通知OpenGL ES 用于绘制的2D渲染表面的原点、宽度和高度。
// 在OpenGL ES 中,视口(Viewport) 定义所有OpenGL ES 渲染操作最终显示的2D矩形
// 视口(Viewport) 由原点坐标(x,y)和宽度(width) 、高度(height)定义。
glViewport(0, 0, mWidth, mHeight);
}
}
// ====================================================================
NAMESPACE_NativeTriangle::NativeTriangle* nativeTriangle;
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_openglesdemo_triangle_HelloTriangleNativeRenderer_nativeSurfaceCreate(
JNIEnv * env, jobject thiz) {
if (nativeTriangle) {
delete nativeTriangle;
nativeTriangle = nullptr;
}
nativeTriangle = new NAMESPACE_NativeTriangle::NativeTriangle();
nativeTriangle->create();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_openglesdemo_triangle_HelloTriangleNativeRenderer_nativeSurfaceChange(
JNIEnv * env, jobject thiz, jint width, jint height) {
if (nativeTriangle != nullptr) {
nativeTriangle->change(width, height);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_openglesdemo_triangle_HelloTriangleNativeRenderer_nativeDrawFrame(
JNIEnv * env, jobject thiz) {
if (nativeTriangle != nullptr) {
nativeTriangle->draw();
}
}
2.5 关于 GLUtils.h
GLUtils.h,是我写的一个工具类,如下所示:
#ifndef OPEN_GL_LESSON_NATIVE_GL_UTILS_H_
#define OPEN_GL_LESSON_NATIVE_GL_UTILS_H_
#include <jni.h>
#include <GLES3/gl3.h>
#include <GLES3/gl3ext.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <android/asset_manager_jni.h>
#include "./graphics/Matrix.h"
#include "./log/LogUtils.h"
#include "./time/TimeUtils.h"
#include "esShapes.h"
#include "esTransform.h"
class GLUtils {
public:
/** * Set Environment parameter */
static void setEnvAndAssetManager(JNIEnv* env, jobject assetManager);
/** * Loads a file from assets/path into a char array. */
static char* openTextFile(const char* path);
/** * Loads a texture from assets/texture/<name> */
static GLuint loadTexture(const char* name);
/** * Create a program with the given vertex and framgent * shader source code. */
static GLuint createProgram(const char** vertexSource, const char** fragmentSource);
static GLfloat* generateCubeData(
float* point1, float* point2