前言
这篇文章不会教你OpenGL基础知识,系统地学习直接去看LearnOpenGL就好了;我这里系列文章主要记录GL ES在Android的一些样例实现,本文代码仓库在这里
之前跟着LearnOpenGL教程学了挺久,但是它是基于windows平台的教程,而在Android上面开发的话还是有一些差别,不过也并不难,核心渲染代码其实没什么差别,只需要额外写一些jni调用和窗口、手势控制、资源管理相关代码。今天我们就从头开始先搭一个简单的架子,然后实现一个简单的三角形绘制,后续文章会在此基础上做一些更有趣的案例复刻。
逻辑分层
java层和native层都有各自的优劣势,常规的做法在java层创建一个GLSurfaceView用于显示以及处理各自手势操作,一个GLSurfaceView.Renderer作为媒介用于调用jni方法进行绘制,具体的渲染部分代码都放在native层去做。 虽然java层也提供了OpenGL的API,但是java代码不利于移植,而且相关的库也不多,最重要的是不利于学习和理解opengl。
着色器、纹理等资源直接放在assets下面,也在native层里面读取使用。
Renderer
先写一个简单的自定义GLSurfaceView,这里很简单,只是提供了一个用于可以重设surface尺寸的方法,以备之后给native层使用
class MyGLSurfaceView(context: Context?, attrs: AttributeSet?) : GLSurfaceView(context, attrs) {
private var baseRenderer: BaseRenderer? = null
private var mRatioWidth = 0
private var mRatioHeight = 0
override fun setRenderer(renderer: Renderer) {
super.setRenderer(renderer)
baseRenderer = renderer as BaseRenderer
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when(event.action) {
MotionEvent.ACTION_DOWN -> baseRenderer?.onTouch(event.x, event.y)
}
return super.onTouchEvent(event)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height)
} else {
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension(width, width * mRatioHeight / mRatioWidth)
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height)
}
}
}
/**
* 重设surfaceView尺寸
*/
fun resetSurfaceSize(width: Int, height: Int) {
post {
mRatioWidth = width
mRatioHeight = height
requestLayout()
}
}
}
接下来是用于渲染的GLSurfaceView.Renderer,这个类主要作是调用native层进行具体的场景渲染
- 一个java层的renderer对应一个native层的renderer比较直观,这里在java层用mNativePtr保存对应native层对象的指针地址,便于管理和访问
- 用type简单区分一下要渲染什么东西,便于切换
- 在类初始化的时候调用nativeInit,在native层保存class info、method id等信息,方便反射调用java层
Java
class BaseRenderer(val type: Int, private val surfaceView: MyGLSurfaceView): GLSurfaceView.Renderer {
private val mNativePtr: Long
companion object {
private external fun nativeInit()
init {
nativeInit()
}
private const val RENDER_TYPE = 100
const val RENDER_TRIANGLE = RENDER_TYPE + 1
const val RENDER_RIPPLE = RENDER_TYPE + 2
const val RENDER_HEART = RENDER_TYPE + 3
}
init {
mNativePtr = nativeCreate(type)
}
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
nativeOnSurfaceCreated()
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
nativeOnSurfaceChanged(width, height)
}
override fun onDrawFrame(gl: GL10) {
nativeOnDrawFrame()
}
fun destroy() {
nativeDestroy()
}
fun onTouch(x: Float, y: Float) {
nativeTouch(x, y)
}
fun resetSurfaceSize(width: Int, height: Int) {
surfaceView.resetSurfaceSize(width, height)
}
private external fun nativeCreate(type: Int): Long
private external fun nativeDestroy()
private external fun nativeOnSurfaceCreated()
private external fun nativeOnSurfaceChanged(width: Int, height: Int)
private external fun nativeOnDrawFrame()
private external fun nativeTouch(x: Float, y: Float)
}
native
native层也声明一个renderer基类,到时候根据type创建不同的子类来渲染
Renderer.h
#ifndef GLSAMPLE_RENDERER_H
#define GLSAMPLE_RENDERER_H
#ifdef __cplusplus
extern "C" {
#endif
#include <jni.h>
#ifdef __cplusplus
}
#endif
#include <GLES3/gl32.h>
#include "Shader.h"
class Renderer {
protected:
Shader shader;
uint32_t VAO, VBO;
int surfaceWidth, surfaceHeight;
public:
// 存一个java层对象的引用方便反射
jobject javaRef;
virtual void onSurfaceCreated() = 0;
virtual void onSurfaceChanged(int width, int height) {
glViewport(0, 0, width, height);
surfaceWidth = width;
surfaceHeight = height;
}
virtual void onDraw() = 0;
// 触摸点坐标,绝对坐标
virtual void onTouch(float x, float y) {
}
virtual ~Renderer() {};
};
#endif //GLSAMPLE_RENDERER_H
renderer对应的jni,这里直接用静态注册了,吐槽一下,AS对于静态JNI注册支持很好,可以支持一键生成以及互相跳转,什么时候静态注册也支持就好了。
render_jni.cpp
#include <string>
#include "core/basic/TriangleRenderer.h"
#include "core/advanced/RippleRenderer.h"
#include "core/advanced/HeartRenderer.h"
#include "core/render_type.h"
const char* renderClassName = "com/greensun/glsample/render/BaseRenderer";
/**
* 从java对象存的地址获取native实例
* @param env
* @param obj
* @return
*/
Renderer* getRenderer(JNIEnv *env, jobject obj) {
jlong addr = env->GetLongField(obj, RenderContext::instance()->renderClsInfo.ptr);
jint type = env->GetIntField(obj, RenderContext::instance()->renderClsInfo.type);
Renderer* renderer = nullptr;
switch (type) {
case RENDER_TRIANGLE:
renderer = reinterpret_cast<TriangleRenderer*>(addr);
break;
case RENDER_RIPPLE:
renderer = reinterpret_cast<RippleRenderer*>(addr);
break;
case RENDER_HEART:
renderer = reinterpret_cast<HeartRenderer*>(addr);
break;
}
return renderer;
}
/**
* 根据类型创建不同renderer
* @param type
* @return
*/
Renderer* createRenderer(jint type) {
Renderer* renderer = nullptr;
switch (type) {
case RENDER_TRIANGLE:
renderer = new TriangleRenderer();
break;
case RENDER_RIPPLE:
renderer = new RippleRenderer();
break;
case RENDER_HEART:
renderer = new HeartRenderer();
break;
}
return renderer;
}
#ifdef __cplusplus
extern "C" {
#endif
#include <jni.h>
JNIEXPORT void JNICALL
Java_com_greensun_glsample_render_BaseRenderer_00024Companion_nativeInit(JNIEnv *env, jobject thiz) {
// 保存类信息方便之后反射
jclass localClass = env->FindClass(renderClassName);
jclass renderCls = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
jfieldID renderPtr = env->GetFieldID(renderCls, "mNativePtr", "J");
jfieldID renderType = env->GetFieldID(renderCls, "type", "I");
jmethodID resetSize = env->GetMethodID(renderCls, "resetSurfaceSize", "(II)V");
RenderContext::instance()->renderClsInfo.clz = renderCls;
RenderContext::instance()->renderClsInfo.ptr = renderPtr;
RenderContext::instance()->renderClsInfo.type = renderType;
RenderContext::instance()->renderClsInfo.resetSize = resetSize;
}
JNIEXPORT jlong JNICALL
Java_com_greensun_glsample_render_BaseRenderer_nativeCreate(JNIEnv *env, jobject obj, jint type) {
auto renderer = createRenderer(type);
renderer->javaRef = env->NewGlobalRef(obj);
return reinterpret_cast<jlong>(renderer);
}
JNIEXPORT void JNICALL
Java_com_greensun_glsample_render_BaseRenderer_nativeDestroy(JNIEnv *env, jobject obj) {
auto renderer = getRenderer(env, obj);
if (renderer) {
env->DeleteGlobalRef(renderer->javaRef);
delete renderer;
}
}
JNIEXPORT void JNICALL
Java_com_greensun_glsample_render_BaseRenderer_nativeOnSurfaceCreated(JNIEnv *env, jobject obj) {
getRenderer(env, obj)->onSurfaceCreated();
}
JNIEXPORT void JNICALL
Java_com_greensun_glsample_render_BaseRenderer_nativeOnSurfaceChanged(JNIEnv *env, jobject obj, jint width, jint height) {
getRenderer(env, obj)->onSurfaceChanged(width, height);
}
JNIEXPORT void JNICALL
Java_com_greensun_glsample_render_BaseRenderer_nativeOnDrawFrame(JNIEnv *env, jobject obj) {
getRenderer(env, obj)->onDraw();
}
JNIEXPORT void JNICALL
Java_com_greensun_glsample_render_BaseRenderer_nativeTouch(JNIEnv *env, jobject obj, jfloat x,
jfloat y) {
getRenderer(env, obj)->onTouch(x, y);
}
#ifdef __cplusplus
}
#endif
RenderContext
除此之外native层可能还会需要一些其他资源,比如需要从assets读取着色器时需要一个AssetManager实例,这要从java层传过去,我们在RenderContext类里面做这些事情。
Java
object RenderContext {
@JvmStatic
fun initialize(context: Context) {
setAssetManager(context.resources.assets)
}
@JvmStatic
fun release() {
nativeRelease()
}
@JvmStatic
private external fun setAssetManager(manager: AssetManager)
@JvmStatic
private external fun nativeRelease()
}
native
RenderContext作为一个单例方便任意一个Renderer使用,除了保存AAssetManager之外还保存了java Renderer的类信息,这样下次反射调用java的时候就不用了在此去获取methodID,可以提高一些效率
#ifndef GLSAMPLE_RENDERCONTEXT_H
#define GLSAMPLE_RENDERCONTEXT_H
#include <android/asset_manager.h>
class RenderContext {
private:
static RenderContext *_instance;
RenderContext() {};
RenderContext(const RenderContext &) {};
RenderContext &operator=(const RenderContext &) {
return *this;
};
public:
JavaVM *jvm = nullptr;
static RenderContext *instance() {
if (_instance == nullptr) {
_instance = new RenderContext();
}
return _instance;
}
AAssetManager *assetManager = nullptr;
void resetSurfaceSize(jobject javaRef, int width, int height);
struct ClsInfo {
jclass clz;
jfieldID ptr;
jfieldID type;
jmethodID resetSize;
} renderClsInfo;
~RenderContext() {
assetManager = nullptr;
jvm = nullptr;
}
};
#endif //GLSAMPLE_RENDERCONTEXT_H
#include "core/Renderer.h"
#include "core/RenderContext.h"
RenderContext* RenderContext::_instance = nullptr;
void RenderContext::resetSurfaceSize(jobject javaRef, int width, int height) {
JNIEnv *jniEnv;
bool detached = jvm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6) == JNI_EDETACHED;
if (!detached) {
jniEnv->CallVoidMethod(javaRef, renderClsInfo.resetSize, width, height);
}
}
最后是RenderContext的jni,这边用的动态注册,由于使用了AAssetManager相关api,记得cmake要加上相关的库链接
#include <jni.h>
#include <android/asset_manager_jni.h>
#include "core/RenderContext.h"
template<class T>
int arrayLen(T &array) {
return (sizeof(array) / sizeof(array[0]));
}
const char *cls_bridge = "com/greensun/glsample/RenderContext";
void set_assets_manager(JNIEnv *env, jclass cls, jobject assetManager) {
// 获取native AAssetManager
auto manager = AAssetManager_fromJava(env, assetManager);
RenderContext::instance()->assetManager = manager;
env->GetJavaVM(&RenderContext::instance()->jvm);
}
void context_release(JNIEnv *env, jclass cls) {
auto context = RenderContext::instance();
delete context;
}
JNINativeMethod methods[] = {
{"setAssetManager", "(Landroid/content/res/AssetManager;)V", (void *) set_assets_manager},
{"nativeRelease", "()V", (void *) context_release},
};
int jniRegisterNativeMethods(JNIEnv *env, const char *className, const JNINativeMethod *methods,
int count) {
int res = -1;
jclass cls = env->FindClass(className);
if (cls != nullptr) {
int ret = env->RegisterNatives(cls, methods, count);
if (ret > 0) {
res = 0;
}
}
env->DeleteLocalRef(cls);
return res;
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
jint result = -1;
if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
return result;
}
jniRegisterNativeMethods(env, cls_bridge, methods, arrayLen(methods));
return JNI_VERSION_1_6;
}
JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved) {
}
绘制三角形
基本的东西都已经有了,我们来写代码画一个三角形试试
读取shader
首先需要写Shader,但是我们好像还没有Shader类,很简单,直接把learnOpenGL教程里面的Shader类拿过来用就好了,但是要改一些东西,因为我们的shader是放在assets里面的,所以要从assets中读取,这里直接读取shader文件为字符串然后构建shader
//
// Created by admin on 2022/10/8.
//
#ifndef GLSAMPLE_ASSETUTIL_H
#define GLSAMPLE_ASSETUTIL_H
#include <android/asset_manager.h>
#include "RenderContext.h"
#include "logger.h"
class AssetUtil {
public:
static char *loadTextAsset(const char *path) {
auto asset = AAssetManager_open(RenderContext::instance()->assetManager, path,
AASSET_MODE_UNKNOWN);
if (!asset) {
LOGE("AssetUtil", "loadTextAsset open error");
return nullptr;
}
off_t len = AAsset_getLength(asset);
char *buffer = new char[len + 1];
uint32_t num = AAsset_read(asset, buffer, len);
AAsset_close(asset);
if (num != len) {
LOGE("AssetUtil", "loadTextAsset read error");
delete[] buffer;
return nullptr;
}
buffer[len] = '\0';
return buffer;
}
};
#endif //GLSAMPLE_ASSETUTIL_H
//
// Created by admin on 2022/10/8.
//
#ifndef GLSAMPLE_SHADER_H
#define GLSAMPLE_SHADER_H
#include <fstream>
#include <GLES3/gl32.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <sstream>
#include <string>
#include "AssetUtil.h"
class Shader {
public:
unsigned int id;
Shader() {
}
// constructor generates the shader on the fly
// ------------------------------------------------------------------------
Shader(const char *vertexPath, const char *fragmentPath, const char *geometryPath = nullptr) {
// 1. retrieve the vertex/fragment source code from filePath
const char* vertexCode = AssetUtil::loadTextAsset(vertexPath);
const char* fragmentCode = AssetUtil::loadTextAsset(fragmentPath);
const char* geometryCode = nullptr;
if (geometryPath != nullptr) {
geometryCode = AssetUtil::loadTextAsset(geometryPath);
}
unsigned int vertex, fragment, geometry;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vertexCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fragmentCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
if (geometryPath != nullptr) {
geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &geometryCode, NULL);
glCompileShader(geometry);
checkCompileErrors(geometry, "GEOMETRY");
}
// shader Program
id = glCreateProgram();
glAttachShader(id, vertex);
glAttachShader(id, fragment);
if (geometryPath != nullptr)
glAttachShader(id, geometry);
glLinkProgram(id);
checkCompileErrors(id, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);
if (geometryPath != nullptr)
glDeleteShader(geometry);
}
// activate the shader
// ------------------------------------------------------------------------
void use() {
glUseProgram(id);
}
void release() {
glDeleteProgram(id);
}
// utility uniform functions
// ------------------------------------------------------------------------
void setBool(const std::string &name, bool value) const {
glUniform1i(glGetUniformLocation(id, name.c_str()), (int)value);
}
// ------------------------------------------------------------------------
void setInt(const std::string &name, int value) const {
glUniform1i(glGetUniformLocation(id, name.c_str()), value);
}
// ------------------------------------------------------------------------
void setFloat(const std::string &name, float value) const {
glUniform1f(glGetUniformLocation(id, name.c_str()), value);
}
void setMat4(const std::string &name, const glm::mat4 &mat) const {
glUniformMatrix4fv(glGetUniformLocation(id, name.c_str()), 1, GL_FALSE, glm::value_ptr(mat));
}
void setVec2(const std::string &name, const glm::vec2 &value) const {
glUniform2fv(glGetUniformLocation(id, name.c_str()), 1, glm::value_ptr(value));
}
void setVec3(const std::string &name, const glm::vec3 &value) const {
glUniform3fv(glGetUniformLocation(id, name.c_str()), 1, glm::value_ptr(value));
}
void setVec3(const std::string &name, float x, float y, float z) const {
glUniform3f(glGetUniformLocation(id, name.c_str()), x, y, z);
}
void bindUniformBlock(const std::string &name, GLuint bindingIndex) {
// 获取uniform块索引
unsigned int index = glGetUniformBlockIndex(id, name.c_str());
std::cout << "uniform block index:" << index << std::endl;
glUniformBlockBinding(id, index, bindingIndex);
}
private:
// utility function for checking shader compilation/linking errors.
// ------------------------------------------------------------------------
void checkCompileErrors(unsigned int shader, const char* type) {
int success;
char infoLog[1024];
if (strcmp(type, "PROGRAM") != 0) {
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
LOGE("Shader", "ERROR::SHADER_COMPILATION_ERROR of type: %s \n %s", type, infoLog);
}
} else {
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
LOGE("Shader", "ERROR::SHADER_LINKING_ERROR of type: %s \n %s", type, infoLog);
}
}
}
};
#endif //GLSAMPLE_SHADER_H
编写shader
AS上可以装glsl插件方便代码提示和高亮
接下来是三角形的shader,非常简单没啥好说的,放在assets下面就好了,注意版本声明要加es
triangle.vert
#version 300 es
layout(location = 0) in vec2 pos;
void main() {
gl_Position = vec4(pos, 0.0f, 1.0f);
}
triangle.frag
#version 300 es
out vec4 FragColor;
void main() {
FragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);
}
渲染
这里也非常的简单,写一个TriangleRenderer继承Renderer
#ifndef GLSAMPLE_TRIANGLERENDERER_H
#define GLSAMPLE_TRIANGLERENDERER_H
#include "core/Renderer.h"
class TriangleRenderer : public Renderer {
virtual void onSurfaceCreated() override;
virtual void onDraw() override;
virtual ~TriangleRenderer();
};
#endif //GLSAMPLE_TRIANGLERENDERER_H
#include "core/basic/TriangleRenderer.h"
void TriangleRenderer::onSurfaceCreated() {
float vertices[] = {
0.5f, -0.5f,
-0.5f, -0.5f,
0.0f, 0.5f
};
shader = Shader("shader/basic/triangle.vert", "shader/basic/triangle.frag");
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindVertexArray(VAO);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glEnableVertexAttribArray(0);
shader.use();
}
void TriangleRenderer::onDraw() {
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(0.1f, 0.1f, 0.1f, 0.0f);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
TriangleRenderer::~TriangleRenderer() {
glDeleteBuffers(1, &VBO);
glDeleteVertexArrays(1, &VAO);
}
剩下的就是在Java层进行调用了,可以去看仓库的里面的代码,就不贴这么多了
结语
这篇文章并没有什么内容,主要是起一个抛砖引玉的作用为后续文章做铺垫,下篇我们来复刻实现一个网络上的有趣案例