OpenGL ES 案例复刻(一) 从框架搭建到画一个三角形

135 阅读7分钟

前言

这篇文章不会教你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插件方便代码提示和高亮 image.png

接下来是三角形的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层进行调用了,可以去看仓库的里面的代码,就不贴这么多了

结语

这篇文章并没有什么内容,主要是起一个抛砖引玉的作用为后续文章做铺垫,下篇我们来复刻实现一个网络上的有趣案例