天天看點

Opengl ES之三角形繪制

在前面我們已經在NDK層搭建好了Opengl ES之EGL環境搭建,也介紹了一些着色器相關的理論知識:Opengl ES之着色器,那麼這次我們就使用已經搭配的EGL繪制一個三角形吧。

在Opengl ES的世界中,無論多複雜的形狀都是由點、線或三角形組成的。是以三角形的繪制在Opengl ES中相當重要,猶比武林高手的内功心法...

C++音視訊開發學習資料:點選領取→音視訊開發(資料文檔+視訊教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

坐标系

在Opengl ES中有很多坐标系,今天我們首先了解一些标準化的裝置坐标。

标準化裝置坐标(Normalized Device Coordinates, NDC),一旦你的頂點坐标已經在頂點着色器中處理過,它們就是标準化裝置坐标了, 标準化裝置坐标是一個x、y和z的值都在-1.0到1.0的之間,任何落在-1和1範圍外的坐标都會被丢棄/裁剪,不會顯示在你的螢幕上。

如下圖,在在标準化裝置坐标中,假設有一個正方形的螢幕,那麼螢幕中心就是坐标原點,左上角就是坐标(-1,1),右下角則是坐标(1,-1)。

Opengl ES之三角形繪制

上代碼

這裡需要說明亮點:

  1. 在後續的實戰例子中,經常會複用到前面介紹的demo的代碼,是以如果是複用之前的代碼邏輯,為了節省篇幅,筆者就不重複貼了。
  2. 在demo中為了簡潔,并沒有開啟子線程作為GL線程,很明顯這是不對,實際開發中都應該開啟子線程對Opengl進行操作。

首先為了後續友善使用,我們在Java層和C++分别建立一個BaseOpengl的基類:

BaseOpengl.java

public class BaseOpengl {

    // 三角形
    public static final int DRAW_TYPE_TRIANGLE = 0;

    public long glNativePtr;
    protected EGLHelper eglHelper;
    protected int drawType;

    public BaseOpengl(int drawType) {
        this.drawType = drawType;
        this.eglHelper = new EGLHelper();
    }

    public void surfaceCreated(Surface surface) {
        eglHelper.surfaceCreated(surface);
    }

    public void surfaceChanged(int width, int height) {
        eglHelper.surfaceChanged(width,height);
    }

    public void surfaceDestroyed() {
        eglHelper.surfaceDestroyed();
    }

    public void release(){
        if(glNativePtr != 0){
            n_free(glNativePtr,drawType);
            glNativePtr = 0;
        }
    }

    public void onGlDraw(){
        if(glNativePtr == 0){
            glNativePtr = n_gl_nativeInit(eglHelper.nativePtr,drawType);
        }
        if(glNativePtr != 0){
            n_onGlDraw(glNativePtr,drawType);
        }
    }

    // 繪制
    private native void n_onGlDraw(long ptr,int drawType);
    protected native long n_gl_nativeInit(long eglPtr,int drawType);
    private native void n_free(long ptr,int drawType);

}           

下面是C++的BaseOpengl:

C++音視訊開發學習資料:點選領取→音視訊開發(資料文檔+視訊教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

BaseOpengl.h

#ifndef NDK_OPENGLES_LEARN_BASEOPENGL_H
#define NDK_OPENGLES_LEARN_BASEOPENGL_H
#include "../eglhelper/EglHelper.h"
#include "GLES3/gl3.h"
#include <string>

class BaseOpengl {
public:
    EglHelper *eglHelper;
    GLint program{0};

public:
    BaseOpengl();
    // 析構函數必須是虛函數
    virtual ~BaseOpengl();
    // 加載着色器并連結成程式
    void initGlProgram(std::string ver,std::string fragment);
    // 繪制
    virtual void onDraw() = 0;
};


#endif //NDK_OPENGLES_LEARN_BASEOPENGL_H           

注意基類的析構函數一定要是虛函數,為什麼?如果不是虛函數的話則會導緻無法完全析構,具體原因請大家面向搜尋引擎程式設計。

BaseOpengl.cpp

#include "BaseOpengl.h"
#include "../utils/ShaderUtils.h"

BaseOpengl::BaseOpengl() {

}

void BaseOpengl::initGlProgram(std::string ver, std::string fragment) {
    program = createProgram(ver.c_str(),fragment.c_str());
}

BaseOpengl::~BaseOpengl(){
    eglHelper = nullptr;
    if(program != 0){
        glDeleteProgram(program);
    }
}           

然後使用BaseOpengl自定義一個SurfaceView,為MyGLSurfaceView:

public class MyGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    public BaseOpengl baseOpengl;
    private OnDrawListener onDrawListener;

    public MyGLSurfaceView(Context context) {
        this(context,null);
    }

    public MyGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        getHolder().addCallback(this);
    }

    public void setBaseOpengl(BaseOpengl baseOpengl) {
        this.baseOpengl = baseOpengl;
    }

    public void setOnDrawListener(OnDrawListener onDrawListener) {
        this.onDrawListener = onDrawListener;
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        if(null != baseOpengl){
            baseOpengl.surfaceCreated(surfaceHolder.getSurface());
        }
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int w, int h) {
        if(null != baseOpengl){
            baseOpengl.surfaceChanged(w,h);
        }
        if(null != onDrawListener){
            onDrawListener.onDrawFrame();
        }
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
        if(null != baseOpengl){
            baseOpengl.surfaceDestroyed();
        }
    }

    public interface OnDrawListener{
        void onDrawFrame();
    }
}           

有了以上基類,既然我們的目标是繪制一個三角形,那麼我們在Java層和C++層再建立一個TriangleOpengl的類吧,他們都繼承TriangleOpengl:

C++音視訊開發學習資料:點選領取→音視訊開發(資料文檔+視訊教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

TriangleOpengl.java

public class TriangleOpengl extends BaseOpengl{

    public TriangleOpengl() {
        super(BaseOpengl.DRAW_TYPE_TRIANGLE);
    }

}           

C++ TriangleOpengl類,TriangleOpengl.h:

#ifndef NDK_OPENGLES_LEARN_TRIANGLEOPENGL_H
#define NDK_OPENGLES_LEARN_TRIANGLEOPENGL_H
#include "BaseOpengl.h"

class TriangleOpengl: public BaseOpengl{
public:
    TriangleOpengl();
    virtual ~TriangleOpengl();
    virtual void onDraw();

private:
    GLint positionHandle{-1};
    GLint colorHandle{-1};
};


#endif //NDK_OPENGLES_LEARN_TRIANGLEOPENGL_H           

TriangleOpengl.cpp:

#include "TriangleOpengl.h"
#include "../utils/Log.h"

// 定點着色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aColor;\n"
                         "in vec4 aPosition;\n"
                         "out vec4 vColor;\n"
                         "void main() {\n"
                         "    vColor = aColor;\n"
                         "    gl_Position = aPosition;\n"
                         "}";

// 片元着色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "in vec4 vColor;\n"
                              "out vec4 fragColor;\n"
                              "void main() {\n"
                              "    fragColor = vColor;\n"
                              "}";

// 三角形三個頂點
const static GLfloat VERTICES[] = {
        0.0f,0.5f,
        -0.5f,-0.5f,
        0.5f,-0.5f
};

// rgba
const static GLfloat COLOR_ICES[] = {
        0.0f,0.0f,1.0f,1.0f
};

TriangleOpengl::TriangleOpengl():BaseOpengl() {
    initGlProgram(ver,fragment);
    positionHandle = glGetAttribLocation(program,"aPosition");
    colorHandle = glGetAttribLocation(program,"aColor");
    LOGD("program:%d",program);
    LOGD("positionHandle:%d",positionHandle);
    LOGD("colorHandle:%d",colorHandle);
}

TriangleOpengl::~TriangleOpengl() noexcept {

}

void TriangleOpengl::onDraw() {
    LOGD("TriangleOpengl onDraw");
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);
    /**
     * size 幾個數字表示一個點,顯示是兩個數字表示一個點
     * normalized 是否需要歸一化,不用,這裡已經歸一化了
     * stride 步長,連續頂點之間的間隔,如果頂點直接是連續的,也可填0
     */
    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,VERTICES);
    // 啟用頂點資料
    glEnableVertexAttribArray(positionHandle);

    // 這個不需要glEnableVertexAttribArray
    glVertexAttrib4fv(colorHandle, COLOR_ICES);

    glDrawArrays(GL_TRIANGLES,0,3);

    glUseProgram(0);

    // 禁用頂點
    glDisableVertexAttribArray(positionHandle);
    if(nullptr != eglHelper){
        eglHelper->swapBuffers();
    }
    LOGD("TriangleOpengl onDraw--end");
}
           

在前面的章節中我們介紹了着色器的建立、編譯、連結等,但是缺少了具體使用方式,這裡我們補充說明一下。

着色器的使用隻要搞懂如何傳遞資料給着色器中變量。首先我們需要擷取到着色器程式中的變量,然後指派。

我們看上面的TriangleOpengl.cpp的構造函數:

TriangleOpengl::TriangleOpengl():BaseOpengl() {
    initGlProgram(ver,fragment);
    // 擷取aPosition變量
    positionHandle = glGetAttribLocation(program,"aPosition");
    // 擷取aColor
    colorHandle = glGetAttribLocation(program,"aColor");
    LOGD("program:%d",program);
    LOGD("positionHandle:%d",positionHandle);
    LOGD("colorHandle:%d",colorHandle);
}           

由上,我們通過函數glGetAttribLocation擷取了變量aPosition和aColor的句柄,這裡我們定義的aPosition和aColor是向量變量,如果我們定義的是uniform統一變量的話,則需要使用函數glGetUniformLocation擷取統一變量句柄。 有了這些變量句柄,我們就可以通過這些變量句柄傳遞函數給着色器程式了,具體可參考TriangleOpengl.cpp的onDraw函數。

此外如果變量是一個統一變量(uniform)的話,則通過一系列的 glUniform...函數傳遞參數。

這裡說明一下函數glVertexAttribPointer的stride參數,一般情況下不會用到,傳遞0即可,但是如果需要提高性能,例如将頂點坐标和紋理/顔色坐标等放在同一個數組中傳遞,則需要使用到這個stride參數了,目前頂點坐标數組和其他數組是分離的,暫時可以不管。

C++音視訊開發學習資料:點選領取→音視訊開發(資料文檔+視訊教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

在Activity中調用一下測試結果:

public class DrawTriangleActivity extends AppCompatActivity {

    private TriangleOpengl mTriangleOpengl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_draw_triangle);
        MyGLSurfaceView glSurfaceView = findViewById(R.id.my_gl_surface_view);
        mTriangleOpengl = new TriangleOpengl();
        glSurfaceView.setBaseOpengl(mTriangleOpengl);
        glSurfaceView.setOnDrawListener(new MyGLSurfaceView.OnDrawListener() {
            @Override
            public void onDrawFrame() {
                mTriangleOpengl.onGlDraw();
            }
        });
    }

    @Override
    protected void onDestroy() {
        if(null != mTriangleOpengl){
            mTriangleOpengl.release();
        }
        super.onDestroy();
    }
}           

如果運作起來,看到一個藍色的三角形,則說明三角形繪制成功啦!

Opengl ES之三角形繪制

如果你對音視訊開發感興趣,或者對本文的一些闡述有自己的看法,可以在下方的留言框給我留言,一起探讨。