目录
为了介绍OpenGL ES 3.0的基本概念,我们从一个简单的示例开始。
这个示例说明创建一个三角形的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, float* point3, float* point4,
float* point5, float* point6, float* point7, float* point8,
int elementsPerPoint);
static void printGLString(const char *name, GLenum s) {
const char *v = (const char *) glGetString(s);
LOGI("GL %s = %s \n", name, v);
}
};
#endif //OPEN_GL_LESSON_NATIVE_GL_UTILS_H_
具体实现是GLUtils.cpp,如下所示:
#include <cstdlib>
#include "GLUtils.h"
static JNIEnv* sEnv = nullptr;
static jobject sAssetManager = nullptr;
static AAsset* loadAsset(const char* path) {
AAssetManager* nativeManager = AAssetManager_fromJava(sEnv, sAssetManager);
if (nativeManager == nullptr) {
return nullptr;
}
return AAssetManager_open(nativeManager, path, AASSET_MODE_UNKNOWN);
}
/**
* Loads the given source code as a shader of the given type.
*
* 负责 加载着色器源代码、编译并检查错误。他返回一个着色器对象
*/
static GLuint loadShader(GLenum shaderType, const char** source) {
// Create the shader object
GLuint shader;
GLint compiled;
// Create the shader object
// shaderType 可以是 GL_VERTEX_SHADER 或者 GL_FRAGMENT_SHADER
shader = glCreateShader(shaderType);
if (shader == 0) {
return 0;
}
// Load the shader source
glShaderSource(shader, 1, source, nullptr);
// Compile the shader
glCompileShader(shader);
// Check the compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = (char*)malloc(sizeof(char) * infoLen);
// 检索信息日志
glGetShaderInfoLog(shader, infoLen, nullptr, infoLog);
LOGE("Error compiling shader:\n%s\n", infoLog);
free(infoLog);
}
// 删除Shader
glDeleteShader(shader);
return 0;
}
return shader;
}
GLuint GLUtils::createProgram(const char** vertexSource, const char** fragmentSource) {
// Load the Vertex shader
GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
// Load the Fragment shader
GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentSource);
if (fragmentShader == 0) {
return 0;
}
// Create the program object
GLuint program = glCreateProgram();
if (program == 0) {
return 0;
}
// Bind the vertex shader to the program
glAttachShader(program, vertexShader);
// Bind the fragment shader to the program.
glAttachShader(program, fragmentShader);
// Link the program
glLinkProgram(program);
// Check the link status
GLint linkStatus;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (!linkStatus) {
// Retrieve compiler error message when linking fails
GLint infoLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = (char*)malloc(sizeof(char) * infoLen);
//获取信息
glGetProgramInfoLog(program, infoLen, nullptr, infoLog);
LOGE("Error linking program:\n%s\n", infoLog);
free(infoLog);
}
// 删除程序对象
glDeleteProgram(program);
return 0;
}
// Free up no longer needed shader resources
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
GLuint GLUtils::loadTexture(const char* path) {
GLuint textureId = 0;
jclass utilsClass = sEnv->FindClass(
"com/oyp/openglesdemo/Utils"); //com.oyp.openglesdemo.Utils类
if (utilsClass == nullptr) {
LOGE("Couldn't find utils class");
return (GLuint)-1;
}
jmethodID loadTexture = sEnv->GetStaticMethodID(utilsClass, "loadTexture",
"(Landroid/content/res/AssetManager;Ljava/lang/String;)I");
if (loadTexture == nullptr) {
LOGE("Couldn't find loadTexture method");
return (GLuint)-1;
}
jstring pathStr = sEnv->NewStringUTF(path);
textureId = (GLuint)sEnv->CallStaticIntMethod(utilsClass, loadTexture, sAssetManager, pathStr);
sEnv->DeleteLocalRef(pathStr);
return textureId;
}
void GLUtils::setEnvAndAssetManager(JNIEnv* env, jobject assetManager) {
sEnv = env;
sAssetManager = assetManager;
}
char* GLUtils::openTextFile(const char* path) {
AAsset* asset = loadAsset(path);
if (asset == nullptr) {
LOGE("Couldn't load %s", path);
return nullptr;
}
off_t length = AAsset_getLength(asset);
char* buffer = new char[length + 1];
int num = AAsset_read(asset, buffer, length);
AAsset_close(asset);
if (num != length) {
LOGE("Couldn't read %s", path);
delete[] buffer;
return nullptr;
}
buffer[length] = '\0';
return buffer;
return NULL;
}
GLfloat* GLUtils::generateCubeData(float* point1,
float* point2,
float* point3,
float* point4,
float* point5,
float* point6,
float* point7,
float* point8,
int elementsPerPoint) {
// Given a cube with the points define as follows:
// front left top, front right top, front left bottom,front right bottom
// back left top, back right top, back left bottom, front right bottom
// return an array of 6 sides, 2 triangles per side, 3 vertices per cube.
int FRONT = 0;
int RIGHT = 1;
int BACK = 2;
int LEFT = 3;
int TOP = 4;
// int BOTTOM = 5;
int size = elementsPerPoint * 6 * 6;
float* cubeData = new float[size];
for (int face = 0; face < 6; face++) {
// Relative to the side,
// p1 = top left
// p2 = top right
// p3 = bottom left
// p4 = bottom right
float* p1, * p2, * p3, * p4;
// Select the points for this face
if (face == FRONT) {
p1 = point1;
p2 = point2;
p3 = point3;
p4 = point4;
}
else if (face == RIGHT) {
p1 = point2;
p2 = point6;
p3 = point4;
p4 = point8;
}
else if (face == BACK) {
p1 = point6;
p2 = point5;
p3 = point8;
p4 = point7;
}
else if (face == LEFT) {
p1 = point5;
p2 = point1;
p3 = point7;
p4 = point3;
}
else if (face == TOP) {
p1 = point5;
p2 = point6;
p3 = point1;
p4 = point2;
}
else // if (face == BOTTOM)
{
p1 = point8;
p2 = point7;
p3 = point4;
p4 = point3;
}
// In OpenGL counter-clockwise winding is default.
// This means that when we look at a triangle,
// if the points are counter-clockwise we are looking at the "front".
// If not we are looking at the back.
// OpenGL has an optimization where all back-facing triangles are culled, since they
// usually represent the backside of an object and aren't visible anyways.
// Build the triangles
// 1---3,6
// | / |
// 2,4--5
int offset = face * elementsPerPoint * 6;
for (int i = 0; i < elementsPerPoint; i++) {
cubeData[offset++] = p1[i];
}
for (int i = 0; i < elementsPerPoint; i++) {
cubeData[offset++] = p3[i];
}
for (int i = 0; i < elementsPerPoint; i++) {
cubeData[offset++] = p2[i];
}
for (int i = 0; i < elementsPerPoint; i++) {
cubeData[offset++] = p3[i];
}
for (int i = 0; i < elementsPerPoint; i++) {
cubeData[offset++] = p4[i];
}
for (int i = 0; i < elementsPerPoint; i++) {
cubeData[offset++] = p2[i];
}
}
return cubeData;
}
三、程序讲解一下
按照上面的步骤的话,我们就可以实现出一个OpenGL ES 3.0的示例,绘制出一个三角形。下面我们来具体讲解一下代码。
3.1 创建简单的顶点和片段着色器
注意:在OpenGL ES 3.0中,除非加载有效的顶点和片段着色器,否则不会绘制任何集合形状。
在之前的博客,我们介绍了OpenGL ES 3.0可编程管线的基础支持,学习了有关顶点和片段着色器的知识。这两个着色器程序描述了顶点变化和片段的绘制。
要进行任何渲染,OpenGL ES 3.0程序必须至少有一个顶点着色器和一个片段着色器。
- 顶点着色器
在NativeTriangle.cpp中,我们定义了一个顶点着色器,如下所示:
// 顶点着色器
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";
- 顶点着色器第一行声明使用了着色器版本(#version 300 es 表示OpenGL ES 着色器语言 V3.00 )
- 这个顶点着色器声明一个输入属性数组------一个名为vPosition的4分量向量。
- layout(location = 0) 限定了这个vPosition变量的位置是顶点属性0。
- 着色器声明了一个main函数,表示着色器执行的开始。
- 着色器主题非常简单:它将vPosition输入属性拷贝到名为gl_Position 的特殊输出变量。每个顶点着色器必须在gl_Position 变量中输出一个位置。这个变量定义传递到管线下一阶段的位置。
- 片段着色器
在NativeTriangle.cpp中,我们定义了一个片段着色器,如下所示:
// 片段着色器
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";
- 正如顶点着色器那样,片段着色器的第1行也声明着色器的版本
- 第2行声明着色器中浮点变量的默认精度。
- 片段着色声明一个输出变量fragColor,这是一个4分量的向量。写入这个变量的值将被输出到颜色缓冲区。在这个例子中,所有片段的着色器输出都是红色( 1.0, 0.0, 0.0, 1.0 )。
一般来说,游戏或者应用程序不会像这个例子一样内嵌着色器源字符串。在大部分现实世界的应用程序中,着色器从某个文本或者数据文件中加载,然后加载到API。 然而,为了简单和示例程序的完整性,我们直接在程序代码中提供了着色器源字符串。
3.2 编译和加载着色器
现在我们已经定义了着色器源代码,可以将着色器加载到OpenGL ES了。
我们在代码中,使用下面的代码加载了顶点着色器和片段着色器。
mProgram = GLUtils::createProgram(&VERTEX_SHADER_TRIANGLE, &FRAGMENT_SHADER_TRIANGLE);
createProgram()
函数调用了loadShader()
函数,loadShader()
函数负责加载着色器源代码、编译并检查错误,如下所示:
/**
* Loads the given source code as a shader of the given type.
*
* 负责 加载着色器源代码、编译并检查错误。他返回一个着色器对象
*/
static GLuint loadShader(GLenum shaderType, const char** source) {
// Create the shader object
GLuint shader;
GLint compiled;
// Create the shader object
// shaderType 可以是 GL_VERTEX_SHADER 或者 GL_FRAGMENT_SHADER
shader = glCreateShader(shaderType);
if (shader == 0) {
return 0;
}
// Load the shader source
glShaderSource(shader, 1, source, nullptr);
// Compile the shader
glCompileShader(shader);
// Check the compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = (char*)malloc(sizeof(char) * infoLen);
// 检索信息日志
glGetShaderInfoLog(shader, infoLen, nullptr, infoLog);
LOGE("Error compiling shader:\n%s\n", infoLog);
free(infoLog);
}
// 删除Shader
glDeleteShader(shader);
return 0;
}
return shader;
}
着色器源代码本身用glShaderSource
函数加载到着色器对象。然后,着色器用glCompileShader
函数编译。
// Load the shader source
glShaderSource(shader, 1, source, nullptr);
// Compile the shader
glCompileShader(shader);
编译着色器之后,确定编译的状态,打印输出生成的错误。
// Check the compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = (char*)malloc(sizeof(char) * infoLen);
// 检索信息日志
glGetShaderInfoLog(shader, infoLen, nullptr, infoLog);
LOGE("Error compiling shader:\n%s\n", infoLog);
free(infoLog);
}
// 删除Shader
glDeleteShader(shader);
return 0;
}
return shader;
}
如果着色器编译成功,则返回一个新的着色器对象,这个对象以后将链接到程序。
3.3 创建一个程序对象并链接着色器
一旦应用程序为顶点和片段着色器创建了着色器对象,就需要创建一个程序对象。从概念上说,程序对象可以视为最终链接的程序。
不同的着色器编译为一个着色器对象之后,他们必须链接到一个程序对象并一起链接,才能绘制图形。
GLuint GLUtils::createProgram(const char** vertexSource, const char** fragmentSource) {
// Load the Vertex shader
GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
// Load the Fragment shader
GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentSource);
if (fragmentShader == 0) {
return 0;
}
// Create the program object
GLuint program = glCreateProgram();
if (program == 0) {
return 0;
}
// Bind the vertex shader to the program
glAttachShader(program, vertexShader);
// Bind the fragment shader to the program.
glAttachShader(program, fragmentShader);
// Link the program
glLinkProgram(program);
// Check the link status
GLint linkStatus;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (!linkStatus) {
// Retrieve compiler error message when linking fails
GLint infoLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = (char*)malloc(sizeof(char) * infoLen);
//获取信息
glGetProgramInfoLog(program, infoLen, nullptr, infoLog);
LOGE("Error linking program:\n%s\n", infoLog);
free(infoLog);
}
// 删除程序对象
glDeleteProgram(program);
return 0;
}
// Free up no longer needed shader resources
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
第一步:是创建程序对象并将顶点着色器和片段着色器链接到对象上。
// Create the program object
GLuint program = glCreateProgram();
if (program == 0) {
return 0;
}
// Bind the vertex shader to the program
glAttachShader(program, vertexShader);
// Bind the fragment shader to the program.
glAttachShader(program, fragmentShader);
最后,我们做好链接程序、检查错误的准备:
// Link the program
glLinkProgram(program);
// Check the link status
GLint linkStatus;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (!linkStatus) {
// Retrieve compiler error message when linking fails
GLint infoLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = (char*)malloc(sizeof(char) * infoLen);
//获取信息
glGetProgramInfoLog(program, infoLen, nullptr, infoLog);
LOGE("Error linking program:\n%s\n", infoLog);
free(infoLog);
}
// 删除程序对象
glDeleteProgram(program);
return 0;
}
// Free up no longer needed shader resources
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
经过上述的所有步骤,我们最终编译了着色器,检查编译错误,创建程序对象,链接着色器,链接程序并检查链接错误。
程序对象成功链接之后,终于可以使用程序对象来渲染了!
我们用glUseProgram
绑定程序对象,进行渲染。
// Use the program object
// 在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
// 当我们渲染一个物体时要使用着色器程序 , 将其设置为活动程序。这样就可以开始渲染了
glUseProgram(mProgram);
用程序对象句柄调用glUseProgram之后,所有后续的渲染将用连接到程序对象的顶点和片段着色器进行。
上面的步骤,可以用下面这个套路图来总结一下:
3.4 设置视口和清除颜色缓冲区
3.4.1 设置视口
我们在 NativeTriangle::change 中调用了下面的代码
// Set the viewport
// 通知OpenGL ES 用于绘制的2D渲染表面的原点、宽度和高度。
// 在OpenGL ES 中,视口(Viewport) 定义所有OpenGL ES 渲染操作最终显示的2D矩形
// 视口(Viewport) 由原点坐标(x,y)和宽度(width) 、高度(height)定义。
glViewport(0, 0, mWidth, mHeight);
这个代码通知OpenGL ES用于绘制的2D渲染表面的原点、高度和宽度。
在OpenGL ES中,视口(ViewPort)定义了所有OpenGL ES渲染操作最终显示的2D矩形。
视口由原点坐标(x,y)和宽度、高度定义。
3.4.2 清除颜色缓冲区
在设置视口之后,下一步是清除屏幕。在OpenGL ES中,绘图中涉及多种缓冲区类型:颜色、深度和模板。
在这个例子中,只向颜色缓冲区中绘制图形。在每个帧的开始,我们用glClear
函数清除颜色缓冲区。
在 NativeTriangle::draw
函数的第一行调用glClear(GL_COLOR_BUFFER_BIT);
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);
...其他代码
}
缓冲区将glClearColor
指定的颜色清除。清除颜色应该由应用程序在调用颜色缓冲区的glClear
之前设置。
例子中,我们在NativeTriangle::create()中设置了清除颜色
// 设置清除颜色
glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
3.5 加载几何形状和绘制图元
清除颜色缓冲区、设置视口和加载程序对象之后,必须指定三角形的几何形状。
3.5.1 加载几何形状
三角形的顶点由vVertices数组中的三个坐标(x,y,z) 指定。
在 OpenGL 中,形状的面是由三维空间中的三个或更多点定义的表面。一个包含三个或更多三维点(在 OpenGL 中被称为顶点)的集合具有一个正面和一个背面。如何知道哪一面为正面,哪一面为背面呢?这个问题问得好!答案与环绕(即您定义形状的点的方向)有关。
在此示例中,三角形的点按照使它们沿逆时针方向绘制的顺序定义。这些坐标的绘制顺序定义了该形状的环绕方向。默认情况下,在 OpenGL 中,沿逆时针方向绘制的面为正面。图 1 中所示的三角形经过了定义,因此您看到的是该形状的正面(根据 OpenGL 解释),而另一面是背面。
// 我们在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::draw()使用这个vVertices数组。
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);
}
顶点位置需要加载到GL,并连接到顶点着色器中声明的vPosition属性。
你应该记得,我们前面已经将vPosition变量与输入属性位置0绑定。
顶点着色器中的每个属性都有一个由无符号整数值唯一标识的位置。
为了将数据加载到顶点属性0,我们调用glVertexAttribPointer
函数。
3.5.1 绘制图元
绘制三角形的最后一步是真正告诉OpenGL ES绘制图元。在这个例子中,我们用glDrawArrays
函数实现这个目的。这个函数绘制三角形、直线或者条带等图元。
3.6 为什么不是等边三角形?
在 Android 设备上显示图形时,一个的基本问题在于屏幕的尺寸和形状各不相同。OpenGL 假设屏幕采用均匀的方形坐标系,默认情况下,您可以将这些坐标恰当地绘制到通常为非方形的屏幕上,就好像这些屏幕是完美的方形一样。
上图中:
- 左边:默认 OpenGL 坐标系(左)
- 右边:典型的 Android 设备屏幕(右)的映射。
上图在左侧显示了针对 OpenGL 帧假定的均匀坐标系,还显示了这些坐标实际上如何映射到右侧屏幕方向为横向的典型设备屏幕。要解决此问题,您可以通过应用 OpenGL 投影模式和相机视图来转换坐标,这样,您的图形对象在任何屏幕上都具有正确的比例。
为了应用投影和相机视图,您可以创建一个投影矩阵和一个相机视图矩阵,并将它们应用到 OpenGL 渲染管道。投影矩阵会重新计算图形的坐标,以便它们能够正确地映射到 Android 设备屏幕。相机视图矩阵会创建一个转换,用于从特定的眼睛位置渲染对象。
四、总结
上面我们实现了一个简单的OpenGL ES 3.0示例:绘制一个三角形。也简单的分析了一下程序源代码,代码有很多细节需要后面再深入讲解。我们可以将此例子作为Hello World来记住即可,记住这个套路。
五、纯java实现
5.1 顶点着色器和片段着色器
如上图所示,我们将顶点着色器和片段着色器放在assets/shaders目录下:
- 顶点着色器 shaders\vertexShader.vert
#version 300 es
// 表示OpenGL ES着色器语言V3.00
// 使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。
// 声明一个输入属性数组:一个名为vPosition的4分量向量
// 在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。
// 在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。
//注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。
layout(location = 0) in vec4 vPosition;
void main()
{
// 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。
// 将vPosition输入属性拷贝到名为gl_Position的特殊输出变量
// 每个顶点着色器必须在gl_Position变量中输出一个位置,这个位置传递到管线下一个阶段的位置
gl_Position = vPosition;
}
- 片段着色器 shaders\fragmentShader.frag
#version 300 es
// 表示OpenGL ES着色器语言V3.00
// 声明着色器中浮点变量的默认精度
precision mediump float;
// 声明一个输出变量fragColor,这是一个4分量的向量,
// 写入这个变量的值将被输出到颜色缓冲器
out vec4 fragColor;
void main()
{
// 所有片段的着色器输出都是红色( 1.0, 0.0, 0.0, 1.0 )
fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );
// 会输出橘黄色
// fragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
5.2 编写Activity
HelloTriangle.java
package com.openglesbook.hellotriangle;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
/**
* Activity class for example program that detects OpenGL ES 3.0.
**/
public class HelloTriangle extends Activity {
private GLSurfaceView mGLSurfaceView;
private final int CONTEXT_CLIENT_VERSION = 3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGLSurfaceView = new GLSurfaceView(this);
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.
mGLSurfaceView.setEGLContextClientVersion(CONTEXT_CLIENT_VERSION);
mGLSurfaceView.setRenderer(new HelloTriangleRenderer(this));
} else {
Log.e("HelloTriangle", "OpenGL ES 3.0 not supported on device. Exiting...");
finish();
}
setContentView(mGLSurfaceView);
}
private boolean detectOpenGLES30() {
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo info = am.getDeviceConfigurationInfo();
return (info.reqGlEsVersion >= 0x30000);
}
@Override
protected void onResume() {
// Ideally a game should implement onResume() and onPause()
// to take appropriate action when the activity looses focus
super.onResume();
mGLSurfaceView.onResume();
}
@Override
protected void onPause() {
// Ideally a game should implement onResume() and onPause()
// to take appropriate action when the activity looses focus
super.onPause();
mGLSurfaceView.onPause();
}
}
注册Activity,定义android:minSdkVersion为18,声明OpenGL ES 3.0 即可。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.openglesbook.hellotriangle"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="18" />
<!-- Tell the system this application requires OpenGL ES 3.0. -->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
<application android:icon="@drawable/icon"
android:allowBackup="false"
android:label="@string/app_name">
<activity android:name=".HelloTriangle"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
5.3 编写GLSurfaceView.Renderer的实现HelloTriangleRenderer
// Hello_Triangle
//
// This is a simple example that draws a single triangle with
// a minimal vertex/fragment shader. The purpose of this
// example is to demonstrate the basic concepts of
// OpenGL ES 3.0 rendering.
package com.openglesbook.hellotriangle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLES30;
import android.opengl.GLSurfaceView;
import android.util.Log;
import com.openglesbook.common.ESShader;
// 可以参考这篇讲解: https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
public class HelloTriangleRenderer implements GLSurfaceView.Renderer {
private Context mContext;
// Member variables
private int mProgramObject;
private int mWidth;
private int mHeight;
private FloatBuffer mVertices;
private static String TAG = "HelloTriangleRenderer";
// 我们在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 环境的一个选项,它允许渲染管道忽略(不计算或不绘制)形状的背面,从而节省时间和内存并缩短处理周期:
private final float[] mVerticesData = {
// 逆时针 三个顶点
0.0f, 0.5f, 0.0f, // 上角
-0.5f, -0.5f, 0.0f, // 左下角
0.5f, -0.5f, 0.0f // 右下角
};
///
// Constructor
//
public HelloTriangleRenderer(Context context) {
mContext = context;
mVertices = ByteBuffer.allocateDirect(mVerticesData.length * 4)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
mVertices.put(mVerticesData).position(0);
}
///
// Initialize the shader and program object
//
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
// Store the program object
// 得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象
mProgramObject = ESShader.loadProgramFromAsset(mContext,
"shaders/vertexShader.vert",
"shaders/fragmentShader.frag");
// 设置清除颜色
GLES30.glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
}
// /
// Draw a triangle using the shader pair created in onSurfaceCreated()
//
public void onDrawFrame(GL10 glUnused) {
// Clear the color buffer
// 清除屏幕
// 在OpenGL ES中,绘图中涉及多种缓冲区类型:颜色、深度、模板。
// 这个例子,绘制三角形,只向颜色缓冲区中绘制图形。在每个帧的开始,我们用glClear函数清除颜色缓冲区
// 缓冲区将用glClearColor指定的颜色清除。
// 这个例子,我们调用了GLES30.glClearColor(1.0f, 1.0f, 1.0f, 0.0f); 因此屏幕清为白色。
// 清除颜色应该由应用程序在调用颜色缓冲区的glClear之前设置。
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// Use the program object
// 在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
// 当我们渲染一个物体时要使用着色器程序 , 将其设置为活动程序。这样就可以开始渲染了
GLES30.glUseProgram(mProgramObject);
// 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定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)。
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, mVertices);
// 现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。
GLES30.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;
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 3);
// 禁用 通用顶点属性数组
GLES30.glDisableVertexAttribArray(0);
}
// /
// Handle surface changes
//
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
mWidth = width;
mHeight = height;
// Set the viewport
// 通知OpenGL ES 用于绘制的2D渲染表面的原点、宽度和高度。
// 在OpenGL ES 中,视口(Viewport) 定义所有OpenGL ES 渲染操作最终显示的2D矩形
// 视口(Viewport) 由原点坐标(x,y)和宽度(width) 、高度(height)定义。
GLES30.glViewport(0, 0, mWidth, mHeight);
}
}
5.4 编写 ESShader工具类
上面的HelloTriangleRenderer,调用了下面的方法来加载顶点着色去和片段着色器,并生成程序对象。
mProgramObject = ESShader.loadProgramFromAsset(mContext,
"shaders/vertexShader.vert",
"shaders/fragmentShader.frag");
ESShader工具类 源代码如下所示:
// The MIT License (MIT)
//
// Copyright (c) 2013 Dan Ginsburg, Budirijanto Purnomo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Book: OpenGL(R) ES 3.0 Programming Guide, 2nd Edition
// Authors: Dan Ginsburg, Budirijanto Purnomo, Dave Shreiner, Aaftab Munshi
// ISBN-10: 0-321-93388-5
// ISBN-13: 978-0-321-93388-1
// Publisher: Addison-Wesley Professional
// URLs: http://www.opengles-book.com
// http://my.safaribooksonline.com/book/animation-and-3d/9780133440133
//
// ESShader
//
// Utility functions for loading GLSL ES 3.0 shaders and creating program objects.
//
package com.openglesbook.common;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import android.content.Context;
import android.opengl.GLES30;
import android.util.Log;
public class ESShader {
/**
* brief Read a shader source into a String
* @param context context
* @param fileName fileName Name of shader file
* @return A String object containing shader source, otherwise null
*/
private static String readShader(Context context, String fileName) {
StringBuilder sb = new StringBuilder();
try {
InputStream is = context.getAssets().open(fileName);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
/**
* brief Load a shader, check for compile errors, print error messages to output log
* @param type Type of shader (GL_VERTEX_SHADER or GL_FRAGMENT_SHADER)
* @param shaderSrc shaderSrc Shader source string
* @return A new shader object on success, 0 on failure
*/
public static int loadShader(int type, String shaderSrc) {
int shader;
int[] compiled = new int[1];
// Create the shader object
// 调用glCreateShader将根据传入的type参数插件一个新的顶点着色器或者片段着色器
shader = GLES30.glCreateShader(type);
if (shader == 0) {
return 0;
}
// Load the shader source
// glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数 着色器真正的源码
GLES30.glShaderSource(shader, shaderSrc);
// Compile the shader
// 编译着色器
GLES30.glCompileShader(shader);
// Check the compile status
// 检测编译时的状态,是编译错误还是编译成功
// pname: 获得信息的参数,可以为
// GL_COMPILE_STATUS
// GL_DELETE_STATUS
// GL_INFO_LOG_LENGTH
// GL_SHADER_SOURCE_LENGTH
// GL_SHADER_TYPE
GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0);
// 如果着色器编译成功,结果将是GL_TRUE。如果编译失败,结果将为GL_FALSE,编译错误将写入信息日志
if (compiled[0] == 0) {
// 用glGetShaderInfoLog检索信息日志
Log.e("ESShader", GLES30.glGetShaderInfoLog(shader));
// 删除着色器对象
GLES30.glDeleteShader(shader);
return 0;
}
return shader;
}
/**
* brief Load a vertex and fragment shader, create a program object, link program. Errors output to log.
* @param vertShaderSrc Vertex shader source code
* @param fragShaderSrc Fragment shader source code
* @return A new program object linked with the vertex/fragment shader pair, 0 on failure
*/
public static int loadProgram(String vertShaderSrc, String fragShaderSrc) {
int vertexShader;
int fragmentShader;
int programObject;
int[] linked = new int[1];
// Load the vertex/fragment shaders
vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertShaderSrc);
if (vertexShader == 0) {
return 0;
}
fragmentShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragShaderSrc);
if (fragmentShader == 0) {
GLES30.glDeleteShader(vertexShader);
return 0;
}
// Create the program object
programObject = GLES30.glCreateProgram();
if (programObject == 0) {
return 0;
}
GLES30.glAttachShader(programObject, vertexShader);
GLES30.glAttachShader(programObject, fragmentShader);
// Link the program
GLES30.glLinkProgram(programObject);
// Check the link status
GLES30.glGetProgramiv(programObject, GLES30.GL_LINK_STATUS, linked, 0);
if (linked[0] == 0) {
Log.e("ESShader", "Error linking program:");
Log.e("ESShader", GLES30.glGetProgramInfoLog(programObject));
GLES30.glDeleteProgram(programObject);
return 0;
}
// Free up no longer needed shader resources
GLES30.glDeleteShader(vertexShader);
GLES30.glDeleteShader(fragmentShader);
return programObject;
}
/**
* brief Load a vertex and fragment shader from "assets", create a program object, link program. Errors output to log.
* @param context context
* @param vertexShaderFileName Vertex shader source file name
* @param fragShaderFileName Fragment shader source file name
* @return A new program object linked with the vertex/fragment shader pair, 0 on failure
*/
public static int loadProgramFromAsset(Context context, String vertexShaderFileName, String fragShaderFileName) {
int vertexShader;
int fragmentShader;
int programObject;
int[] linked = new int[1];
String vertShaderSrc = null;
String fragShaderSrc = null;
// Read vertex shader from assets
vertShaderSrc = readShader(context, vertexShaderFileName);
System.out.println(" vertShaderSrc = " + vertShaderSrc);
if (vertShaderSrc == null) {
return 0;
}
// Read fragment shader from assets
fragShaderSrc = readShader(context, fragShaderFileName);
System.out.println(" fragShaderSrc = " + fragShaderSrc);
if (fragShaderSrc == null) {
return 0;
}
// Load the vertex shader
vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertShaderSrc);
if (vertexShader == 0) {
GLES30.glDeleteShader(vertexShader);
return 0;
}
// Load the fragment shader
fragmentShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragShaderSrc);
if (fragmentShader == 0) {
GLES30.glDeleteShader(fragmentShader);
return 0;
}
// Create the program object
programObject = GLES30.glCreateProgram();
if (programObject == 0) {
return 0;
}
// 在OpenGL ES3.0中,每个程序对象必须连接一个顶点着色器和一个片段着色器
// 把之前编译的着色器附加到程序对象上
// 着色器可以在任何时候连接-----在连接到程序之前不一定需要编译,甚至可以没有源代码。
// 唯一要求是:每个程序对象必须有且只有一个顶点着色器和一个片段着色器与之连接
// 除了连接着色器之外,你还可以用glDetachShader断开着色器的连接
GLES30.glAttachShader(programObject, vertexShader);
GLES30.glAttachShader(programObject, fragmentShader);
// Link the program
// 链接操作负责生成最终的可执行的程序。
// 一般来说,链接阶段是生成在硬件上运行的最终硬件指令的时候
GLES30.glLinkProgram(programObject);
// Check the link status 检测链接着色器程序是否失败
// pname 获取信息的参数,可以是
// GL_ACTIVE_ATTRIBUTES 返回顶点着色器中活动属性的数量
// GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 返回最大属性名称的最大长度(以字符数表示),这一信息用于确定存储属性名字符串所需的内存量
// GL_ACTIVE_UNIFORM_BLOCK 返回包含活动统一变量的程序中的统一变量块数量
// GL_ACTIVE_UNIFORM_BLOCK_MAX_LENGTH 返回包含活动统一变量的程序中的统一变量块名称的最大长度
// GL_ACTIVE_UNIFORMS 返回活动统一变量的数量
// GL_ACTIVE_UNIFORM_MAX_LENGTH 返回最大统一变量名称的最大长度
// GL_ATTACHED_SHADERS 返回连接到程序对象的着色器数量
// GL_DELETE_STATUS 查询返回程序对象是否已经标记为删除
// GL_LINK_STATUS 检查链接是否成功
// GL_INFO_LOG_LENGTH 程序对象存储的信息日志长度
// GL_LINK_STATUS 链接是否成功
// GL_PROGRAM_BINARY_RETRIEVABLE_HINT 返回一个表示程序目前是否启用二进制检索提示的值
// GL_TRANSFORM_FEEDBACK_BUFFER_MODE 返回GL_SEPARATE_ATTRIBS 或 GL_INTERLEAVED_ATTRIBS 表示变化反馈启用时的缓冲区模式
// GL_TRANSFORM_FEEDBACK_VARYINGS 返回程序的变化反馈模式中捕捉的输出变量
// GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 返回程序的变化反馈模式中捕捉的输出变量名称的最大长度
// GL_VALIDATE_STATUS 查询最后一个校验操作的状态
GLES30.glGetProgramiv(programObject, GLES30.GL_LINK_STATUS, linked, 0);
if (linked[0] == 0) {
Log.e("ESShader", "Error linking program:");
// 获取着色器对象的信息日志
Log.e("ESShader", GLES30.glGetProgramInfoLog(programObject));
// 删除一个程序对象
GLES30.glDeleteProgram(programObject);
return 0;
}
// Free up no longer needed shader resources
GLES30.glDeleteShader(vertexShader);
GLES30.glDeleteShader(fragmentShader);
return programObject;
}
}
效果展示
5.5 Java 和 C++ Native方式对比
-
Native 实现方式和 Java 实现方式原理都是一样的,包括方法名都基本是一样的
可以发现使用java也可以实现,android的android.opengl包实现了相关的opengl功能,实际上最终还是调用C的方法。
-
使用Native方式实现比Java实现更加安全,因为Native方式最终会生成so文件。相对来说更安全点。
-
学习使用Native方式更接近OpenGL ES 的本质,结合《OpenGL ES 3.0编程指南》也更容易看得懂书上的内容,对应起来。
-
Native方式理论上来说比Java方式效率更高一点。但是这个例子自测了一些,测试打开页面的时长结果差别不大。
六、源代码
java版本代码: https://github.com/ouyangpeng/opengles3-book
Native版本代码:https://github.com/ouyangpeng/OpenGLESDemo