在前面我們已經在NDK層搭建好了EGL環境,也介紹了一些著色器相關的理論知識,那麼這次我們就使用已經搭配的EGL繪製一個三角形吧。
在Opengl ES的世界中,無論多複雜的形狀都是由點、線或三角形組成的。因此三角形的繪製在Opengl ES中相當重要,猶比武林高手的內功心法...
在Opengl ES中有很多座標系,今天我們首先了解一些標準化的裝置座標。
標準化裝置座標(Normalized Device Coordinates, NDC),一旦你的頂點座標已經在頂點著色器中處理過,它們就是標準化裝置座標了,
標準化裝置座標是一個x、y和z的值都在-1.0到1.0的之間,任何落在-1和1範圍外的座標都會被丟棄/裁剪,不會顯示在你的螢幕上。
如下圖,在在標準化裝置座標中,假設有一個正方形的螢幕,那麼螢幕中心就是座標原點,左上角就是座標(-1,1),右下角則是座標(1,-1)。
這裡需要說明亮點:
首先為了後續方便使用,我們在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:
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:
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引數了,目前頂點座標陣列和其他陣列是分離的,暫時可以不管。
在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();
}
}
如果執行起來,看到一個藍色的三角形,則說明三角形繪製成功啦!
想來還是不貼原始碼連結了,紙上得來終覺淺,絕知此事要躬行。很多時候就是這樣,你看著覺得很簡單,實際如何還得動手敲,只有在敲的過程中出了問題,然後你解決了,只是才算是你的。
在這個系列完畢後再貼出整個專案demo的程式碼吧。。。
關注我,一起進步,人生不止coding!!!