繪製一個三角形
正如我們學習一門編程語言時大多數教程都會先告訴你怎麼寫出一句 Hello World
,OpenGL的教程大多數第一課也是教你如何繪製一個簡單三角形。接下來我們就按照上述所說的渲染過程,講解一下如何通過OpenGL ES的API在Android手機上顯示出一個三角形。
在Demo中我們創建一個 TriangleActivity
作爲我們的界面,使用Android自帶的 GLSurfaceView
作爲渲染的載體(現在自己創建EGLSurface還爲時過早),同時我們創建一個 Shape
作爲GLSurfaceView的Renderer抽象基礎類 ,讓其實現Renderer,在其子類裏面實現實際的渲染操作
public abstract class Shape implements GLSurfaceView.Renderer {
public Shape(){
}
public int loadShader(final String strSource, final int iType) {
//........
}
public int loadProgram(final String strVSource, final String strFSource) {
//.........
}
/**
* 回收資源
*/
public abstract void destroy() ;
}
可以看到我們在基類中定義了一個抽象方法destroy()
和兩個僞代碼實現的方法
loadShader()
和loadProgram()
我們先來簡單介紹一下OpenGL中編譯着色器並鏈接至最終的Program上的流程:
public int loadShader(final String strSource, final int iType) {
int[] compiled = new int[1];
//創建指定類型的着色器
int iShader = GLES20.glCreateShader(iType);
//將源碼添加到iShader並編譯它
GLES20.glShaderSource(iShader, strSource);
GLES20.glCompileShader(iShader);
//獲取編譯後着色器句柄存在在compiled數組容器中
GLES20.glGetShaderiv(iShader, GLES20.GL_COMPILE_STATUS, compiled, 0);
//容錯判斷
if (compiled[0] == 0) {
Log.d("Load Shader Failed", "Compilation\n" + GLES20.glGetShaderInfoLog(iShader));
return 0;
}
return iShader;
}
public int loadProgram(final String strVSource, final String strFSource) {
int iVShader;
int iFShader;
int iProgId;
int[] link = new int[1];
//獲取編譯後的頂點着色器句柄
iVShader = loadShader(strVSource, GLES20.GL_VERTEX_SHADER);
if (iVShader == 0) {
Log.d("Load Program", "Vertex Shader Failed");
return 0;
}
//獲取編譯後的片元着色器句柄
iFShader = loadShader(strFSource, GLES20.GL_FRAGMENT_SHADER);
if (iFShader == 0) {
Log.d("Load Program", "Fragment Shader Failed");
return 0;
}
//創建一個Program
iProgId = GLES20.glCreateProgram();
//添加頂點着色器與片元着色器到Program中
GLES20.glAttachShader(iProgId, iVShader);
GLES20.glAttachShader(iProgId, iFShader);
//鏈接生成可執行的Program
GLES20.glLinkProgram(iProgId);
//獲取Program句柄,並存在在link數組容器中
GLES20.glGetProgramiv(iProgId, GLES20.GL_LINK_STATUS, link, 0);
//容錯
if (link[0] <= 0) {
Log.d("Load Program", "Linking Failed");
return 0;
}
//刪除已鏈接後的着色器
GLES20.glDeleteShader(iVShader);
GLES20.glDeleteShader(iFShader);
return iProgId;
}
上述代碼中關鍵代碼點都有註釋。 到這裏我們已經獲取到了一個`Program`。
第一個Shape
首先我們現在創建並實現整個渲染過程中最核心的部分 Triangle
,並讓其繼承Shape類。
Renderer接口中有三個需要實現的方法,分別是 onSurfaceCreated
, onSurfaceChanged
以及 onDrawFrame
,前兩個方法如果有接觸過SurfaceView及SurfaceHolder的話就比較熟悉,分別是Surface創建時的回調以及SUrface如寬高變化時的回調, onSurfaceCreated
主要用於 初始化
等, onSurfaceChanged
主要用於做 模型視圖轉換
等操作,而 onDrawFrame
就是當OpenGL渲染每一幀的回調方法,我們的實際繪製操作就在這裏進行。
這三個方法我們先放着,先來按照渲染流程,我們創建繪製一個三角形所需要的 頂點數據
。
頂點數據是一個包含了所繪製圖像放置在OpenGL座標系中後,其各個頂點的 三維座標
的數組(其實頂點數據還可以放置顏色等,通過偏移來獲取不同類型的數據)。那麼剛剛在座標系中說了,OpenGL裏有多個座標系,但是和我們目前關係最大的是NDC,NDC座標系:
即NDC座標系的原點(0,0)默認位置在屏幕中心,x,y,z軸範圍爲[-1,1],而Android屏幕座標系原點在左上角,x,y軸範圍爲[0,各軸分辨率]。
現在我們要繪製一個三角形,頂點在y軸正向最大值位置,左下角在x軸負向最大值位置,右下角在x軸正向最大值位置,那麼對應的頂點數組爲:
//設置三角形頂點數組,默認按逆時針方向繪製
public static float[] triangleCoords = {
0.0f, 1.0f, 0.0f, // 頂點
-1.0f, -0.0f, 0.0f, // 左下角
1.0f, -0.0f, 0.0f // 右下角
};
接下來我們開始編寫頂點着色器:
//頂點着色器
public static final String VERTEX_SHADER =
"//根據所設置的頂點數據,插值在光柵化階段進行\n" +
"attribute vec4 vPosition;" +
"void main() {" +
" //設置最終座標\n" +
" gl_Position = vPosition;" +
"}";
對於上述着色器只需要知道`vPosition`就是我們所設置的頂點數據,而`gl_Position`是OpenGL的內置變量,代表着當前這個片元最終所處的座標。而`vec`是代表向量,座標使用`vec4`而不是`vec3`的原因是因爲`齊次座標`的關係,但是這個在這裏不是重點。 `組裝圖`,`光柵化圖元`OpenGL會自動進行,這裏我們不管,接下來我們開始編寫片元着色器,來爲這個三角形加上顏色:
//片元着色器
public static final String FRAGMENT_SHADER =
"//設置float類型默認精度,頂點着色器默認highp,片元着色器需要用戶聲明\n" +
"precision mediump float;" +
"//顏色值,vec4代表四維向量,此處由用戶傳入,數據格式爲{r,g,b,a}\n" +
"uniform vec4 vColor;" +
"void main() {" +
"//該片元最終顏色值\n" +
"gl_FragColor = vColor;" +
"}";
在上述着色器代碼中,首先我們聲明瞭片元着色器中默認float類型變量的精度(中等),在頂點着色器中默認精度爲highp,而片元着色器中必須自己設置。 之後我們聲明瞭一個`uniform`類型的四維向量,用以存儲用戶所設置的三角形顏色,`gl_FragColor`也是OpenGL的內置變量,表示片元最終的顏色值,這裏我們直接將`vColor`作爲最終顏色。 從片元着色器中可以看到,有一個數據還需要用戶自己設定,那就是三角形的顏色值,格式是{r,g,b,a},設置如下:
// 設置三角形顏色和透明度(r,g,b,a),綠色不透明
public static float[] color = {0.0f, 1.0f, 0f, 1.0f};
最後`寫入幀緩衝區`,`顯示到屏幕上`也是由OpenGL自動完成,那麼至此我們已經完成了頂點着色器和片元着色器的實現,也提供了這兩個着色器所需要的頂點數據和顏色數據,那麼接下來就是怎麼將這些數據與着色器內的變量相綁定,並且告知OpenGL什麼時候開始渲染以及怎麼渲染。 讓我們回到Renderer那三個未實現的接口上,首先我們在`onSurfaceCreated`調用時,也就是Surface正式創建後,做一些初始化操作:
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mProgramId = loadProgram(VERTEX_SHADER, FRAGMENT_SHADER);
//通過OpenGL程序句柄查找獲取頂點着色器中的位置句柄
mPositionId = GLES20.glGetAttribLocation(mProgramId, "vPosition");
//通過OpenGL程序句柄查找獲取片元着色器中的顏色句柄
mColorId = GLES20.glGetUniformLocation(mProgramId, "vColor");
}
還有後續需要綁定我們數據的vColor
,vPosition
的地址。接下來我們需要設置視口來告訴OpenGL我們想要顯示在屏幕的哪個區域內:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0,0,width,height);
}
當我們設置GLSurfaceView爲全屏的時候,那麼上述的`width`就是屏幕寬度,`height`就是屏幕高度,上述設置的意思就是我們當前渲染的視口區域從屏幕左上角原點(0,0)開始,寬高爲全屏。 至此就萬事俱備了,接下來我們便要在OpenGL開始渲染的回調接口`onDrawFrame()`中進行我們最後的渲染操作了:
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT|GLES20.GL_DEPTH_BUFFER_BIT);
//告知OpenGL所要使用的Program
GLES20.glUseProgram(mProgramId);
//啓用指向三角形頂點數據的句柄
GLES20.glEnableVertexAttribArray(mPositionId);
//綁定三角形的座標數據
GLES20.glVertexAttribPointer(mPositionId, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
VERTEX_STRID, vertexBuffer);
//綁定顏色數據
GLES20.glUniform4fv(mColorId, 1, Triangle.color, 0);
//繪製三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, VERTEX_COUNT);
//禁用指向三角形的頂點數據
GLES20.glDisableVertexAttribArray(mPositionId);
}
首先這裏我們注意到除了註釋之外,我們的代碼少了一個變量,就是vertexBuffer,這個變量是一個FloatBuffer類型的變量,用於開闢處一塊內存緩衝區來存儲供OpenGL使用的頂點數據,在我們這個Demo中頂點數據不會發生變化,所以我們直接在onSurfaceCreated()的最後加上如下代碼進行初始化即可:
// 初始化頂點字節緩衝區,用於存放形狀的座標,每個浮點數佔用4個字節
ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4);
//設置使用設備硬件的原生字節序
bb.order(ByteOrder.nativeOrder());
//從ByteBuffer中創建一個浮點緩衝區
vertexBuffer = bb.asFloatBuffer();
// 把座標都添加到FloatBuffer中
vertexBuffer.put(triangleCoords);
//設置buffer從第一個座標開始讀
vertexBuffer.position(0);
還有記得在接口外面聲明變量:
//folat緩衝區
public FloatBuffer vertexBuffer;
爲什麼使用java的nio包下的Buffer作爲內存緩衝區的形式一方面是出於性能等方面的考慮,另一方面 OpenGL 是一個非常底層的繪製接口,它所使用的緩衝區存儲結構是和我們的 Java 程序中不相同的(Java 是大端字節序(BigEdian),而 OpenGL 所需要的數據是小端字節序(LittleEdian))。所以,我們在將 Java 的緩衝區轉化爲 OpenGL 可用的緩衝區時需要作這樣的一些工作。 而顏色的綁定我們看到就簡單得多,只需要調用接口就可以實現,因爲兩者的在這個Demo中變量類型不同(`attribute`只能在頂點着色器中使用,通常用於表示頂點座標、紋理座標等,而`uniform`常用於表示常量形式的顏色、矩陣、材質等,兩者設置接口也不同,具體會在後續着色器章節中講述)。我們也可以通過將顏色與頂點數據放置一起,然後一起轉爲FloatBuffer來傳遞給OpenGL,並且設置每個頂點的顏色不同,通過`glVertexPointer`與`glColorPointer`兩個接口配合使用來繪製出如下的三角形,這也就是之前一直講的插值的含義,OpenGL會自動對頂點間座標以及顏色進行插值計算:
至此我們已經完成`TriangleRender`的實現,最後只需要加上一個回收資源的方法:
@Override
public void destroy() {
GLES20.glDeleteProgram(mProgramId);
}
GLSurfaceView
前面我們完成了 Triangle
的實現,那麼接下來我們將其與 GLSurfaceView
綁定起來以便於看到我們渲染的結果。
爲了簡單起見,我們直接 TriangleActivity
的佈局文件中加入 GLSurfaceView
,在 onCreate()
中加入如下代碼:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGLView = findViewById(R.id.mGLView);
mGLView.setEGLContextClientVersion(2);
render = new Triangle();
mGLView.setRenderer(render);
mGLView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
最後我們跑一下項目,就可以在手機上看到三角形了,接下來的章節中我們再一起去繪製其他Shape,如正方形,原型,三維圖形等
總結
這章我們介紹了OpenGL(ES)以及EGL的相關內容和一些基本概念,同時通過繪製一個簡單的三角形來了解了OpenGL的常見繪製流程以及部分接口,感興趣的可以自己嘗試一下如何繪製一個漸變的三角形或者一個正方形等較簡單的幾何圖形。使用OpenGL進行繪製的確比直接使用Android自帶的繪圖API繁瑣一些,出現了問題也比較難以排查,因爲更接近於底層所以理解上很多地方不太一樣,但是對於圖形渲染或者處理,OpenGL無論是性能還是可以實現的效果都是勝出一籌的。