前言
隨著VR/AR技術(shù)的普及,人機(jī)交互的模式將產(chǎn)生新的變革。OpenGL ES作為移動端上的圖像渲染框架,將變得越來越重要。在此將學(xué)習(xí)OpenGL ES作為Q3的主要目標(biāo)。在10月1日前,希望能有階段性成果。
快速開始
判斷設(shè)備是否支持OpenGL ES
fun checkSupported() : Boolean{
var supportsEs2 = false;
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val configurationInfo = activityManager.getDeviceConfigurationInfo();
supportsEs2 = configurationInfo.reqGlEsVersion >= 0x2000;
val isEmulator = Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
&& (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86"));
supportsEs2 = supportsEs2 || isEmulator
return supportsEs2
}
生命周期
override fun onPause() {
super.onPause()
glSurfaceView.let { glSurfaceView.onPause() }
}
override fun onResume() {
super.onResume()
glSurfaceView.let { glSurfaceView.onResume() }
}
用OpenGL渲染Activity
lateinit var glSurfaceView: GLSurfaceView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (checkSupported()) {
glSurfaceView = GLSurfaceView(this);
glSurfaceView.let { glSurfaceView.setRenderer(GLRender2())
setContentView(glSurfaceView); }
} else {
Toast.makeText(this, "當(dāng)前設(shè)備不支持OpenGL ES 2.0!", Toast.LENGTH_SHORT).show();
}
}
我們可以看到,OpenGL實際的渲染邏輯,全部封裝在了我自己創(chuàng)建的GLRender2中。
以上代碼,就是OpenGL渲染Activity最簡單的外部框架。
渲染邏輯
public class GLRender2 implements GLSurfaceView.Renderer {
private float[] mTriangleArray = {
0f, 1f, 0f,
-1f, -1f, 0f,
1f, -1f, 0f
};
//三角形各頂點顏色(三個頂點)
private float[] mColor = new float[]{
1, 1, 0, 1,
0, 1, 1, 1,
1, 0, 1, 1
};
private FloatBuffer mTriangleBuffer;
private FloatBuffer mColorBuffer;
public GLRender2() {
Log.d("GLRender2" , "call GLRender init");
//點相關(guān)
//先初始化buffer,數(shù)組的長度*4,因為一個float占4個字節(jié)
ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
//以本機(jī)字節(jié)順序來修改此緩沖區(qū)的字節(jié)順序
bb.order(ByteOrder.nativeOrder());
mTriangleBuffer = bb.asFloatBuffer();
//將給定float[]數(shù)據(jù)從當(dāng)前位置開始,依次寫入此緩沖區(qū)
mTriangleBuffer.put(mTriangleArray);
//設(shè)置此緩沖區(qū)的位置。如果標(biāo)記已定義并且大于新的位置,則要丟棄該標(biāo)記。
mTriangleBuffer.position(0);
//顏色相關(guān)
ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
bb2.order(ByteOrder.nativeOrder());
mColorBuffer = bb2.asFloatBuffer();
mColorBuffer.put(mColor);
mColorBuffer.position(0);
}
@Override
public void onDrawFrame(GL10 gl) {
Log.d("GLRender2" , "call onDrawFrame");
// 清除屏幕和深度緩存
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 重置當(dāng)前的模型觀察矩陣
gl.glLoadIdentity();
// 允許設(shè)置頂點
//GL10.GL_VERTEX_ARRAY頂點數(shù)組
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允許設(shè)置顏色
//GL10.GL_COLOR_ARRAY顏色數(shù)組
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
//將三角形在z軸上移動
gl.glTranslatef(0f, 0.0f, -2.0f);
// 設(shè)置三角形
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
// 設(shè)置三角形顏色
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
// 繪制三角形
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
// 取消顏色設(shè)置
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
// 取消頂點設(shè)置
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
//繪制結(jié)束
gl.glFinish();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.d("GLRender2" , "call onSurfaceChanged");
float ratio = (float) width / height;
// 設(shè)置OpenGL場景的大小,(0,0)表示窗口內(nèi)部視口的左下角,(w,h)指定了視口的大小
gl.glViewport(0, 0, width, height);
// 設(shè)置投影矩陣
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩陣
gl.glLoadIdentity();
// 設(shè)置視口的大小
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
//以下兩句聲明,以后所有的變換都是針對模型(即我們繪制的圖形)
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d("GLRender2" , "call onSurfaceCreated");
// 設(shè)置白色為清屏
gl.glClearColor(1, 1, 1, 1);
}
}
以上代碼,渲染出一個變色的三角形:

詳細(xì)介紹
GLRender2
這個平平無奇的三角形,它的渲染邏輯究竟是什么樣的呢?
在此之前,我們需要先了解GLRender2是一個怎樣的類。
GLRender2實現(xiàn)了GLSurfaceView.Renderer接口。需要實現(xiàn)三個方法:
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d("GLRender" , "call onSurfaceCreated");
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.d("GLRender" , "call onSurfaceChanged");
}
@Override
public void onDrawFrame(GL10 gl) {
Log.d("GLRender" , "call onDrawFrame");
}
生命周期
這是GLSurfaceView生命周期的三個環(huán)節(jié)。
onSurfaceCreated
onSurfaceCreated在GLRender2被初始化后首先調(diào)用。通常用于初始化伴隨GLSurfaceView整個生命周期的數(shù)據(jù)和設(shè)置初始顏色。
onSurfaceChanged
onSurfaceChanged
當(dāng)GLSurfaceView大小改變時,對應(yīng)的Surface大小也會改變。值得注意的是,在Surface剛創(chuàng)建的時候,它的size其實是0,也就是說在畫第一次圖之前它也會被調(diào)用一次的。(而且對于很多時候,Surface的大小是不會改變的,那么此函數(shù)就只在創(chuàng)建之初被調(diào)用一次而已)
原型如下:
public abstract void onSurfaceChanged (GL10 gl, int width, int height)
另外值得注意的是,它告訴了我們這張紙有多高多寬。這點很重要。因為在onSurfaceCreated的時候我們是不知道紙的寬高的,所以有一些和長寬相關(guān)的初始化工作還得在此函數(shù)中來做。
onDrawFrame
以后會有兩種模式供你選擇:
- RENDERMODE_CONTINUOUSLY
- RENDERMODE_WHEN_DIRTY
第一種模式(RENDERMODE_CONTINUOUSLY):
連續(xù)不斷的刷,畫完一幅圖馬上又畫下一幅。這種模式很明顯是用來畫動畫的;
第二種模式(RENDERMODE_WHEN_DIRTY):
只有在需要重畫的時候才畫下一幅。這種模式就比較節(jié)約CPU和GPU一些,適合用來畫不經(jīng)常需要刷新的情況。多說一句,系統(tǒng)如何知道需要重畫了呢?當(dāng)然是你要告訴它……
調(diào)用GLSurfaceView的requestRender ()方法,使其重繪。
GLSurfaceView的setRenderMode(int renderMode)方法。可以供你設(shè)置你需要的刷新模式。
設(shè)置背景色
// 設(shè)置白色為清屏
gl.glClearColor(1, 1, 1, 1);
設(shè)置場景大小
// 設(shè)置OpenGL場景的大小,(0,0)表示窗口內(nèi)部視口的左下角,(w,h)指定了視口的大小
gl.glViewport(0, 0, width, height);
設(shè)置投影矩陣
在渲染中,我們只繪制可見的東西。所以我們需要將真實物體轉(zhuǎn)化到可見區(qū)域,即謂之投影矩陣。
// 設(shè)置投影矩陣
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩陣
gl.glLoadIdentity();
// 設(shè)置視口的大小
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
這三句將真實物體映射到坐標(biāo)系中。這個地方可能有點難以理解。
雖然在OpenGL中,我們畫的是3D物體,但手機(jī)屏幕畢竟是一個平面。我們在生活中,看見的也只是一個平面。那么,一個3D物體,我們看到的應(yīng)該是什么樣的,取決于我們的投影矩陣如何設(shè)置。
假設(shè),我們的三角形,三個點分別是:
private float[] mTriangleArray = {
0f, 1f, -2f,
-1f, -1f, -2f,
1f, -1f, -2f
};
那么,這個三角形其實是在z軸為-2處的一個平面。我們用下圖的方式,進(jìn)行觀察。

下圖,近處的平面,距離視點為1,遠(yuǎn)處的為10。我們畫的三角平面,就在距離視點2的位置。在距離視點1處,我們的視口大小是 2ratio x 2。到距離2處,我們的視口大小一定為2ratio x 2。
所以此時,我們渲染我們的三角形,它的高一定為畫布高度的1/2。
如果我們將近平面,視點距離改為0.5f。同樣的三角形,我們渲染出來高度一定為畫布高度的1/4。
如果我們將三角形改為:
private float[] mTriangleArray = {
0f, 1f, -1f,
-1f, -1f, -2f,
1f, -1f, -2f
};
視點距離改為1.0f 。三角形的高度將變?yōu)楫嫴几叨鹊?/4。
而遠(yuǎn)平面的視點距離,則決定了我們可以看到多遠(yuǎn)的元素。比如我們還是三角形為:
private float[] mTriangleArray = {
0f, 1f, -1f,
-1f, -1f, -2f,
1f, -1f, -2f
};
將遠(yuǎn)平面視點距離改為1.5f,此時我們將只能看到上個例子中三角形的上半部分。
其中變化讀者可以畫立體圖,慢慢感受一下。
回歸模型
完成了對投影的操作后,我們將操作模式設(shè)置到模型操作。
//以下兩句聲明,以后所有的變換都是針對模型(即我們繪制的圖形)
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
完成了種種矩陣的設(shè)置后,我們可以開始進(jìn)行繪制了。
圖形和色彩數(shù)據(jù)
OpenGL并不是對堆里面的數(shù)據(jù)進(jìn)行操作,而是在直接內(nèi)存中(Direct Memory),即操作的數(shù)據(jù)需要保存到NIO里面的Buffer對象中。而我們上面聲明的float[]對象保存在堆中,因此,需要我們將float[]對象轉(zhuǎn)為java.nio.Buffer對象。
private float[] mTriangleArray = {
0f, 1f, 1f,
-1f, -1f, 0f,
1f, -1f, 0f
};
//三角形各頂點顏色(三個頂點)
private float[] mColor = new float[]{
1, 1, 0, 1,
0, 1, 1, 1,
1, 0, 1, 1
};
private FloatBuffer mTriangleBuffer;
private FloatBuffer mColorBuffer;
public GLRender2() {
Log.d("GLRender2" , "call GLRender init");
//點相關(guān)
//先初始化buffer,數(shù)組的長度*4,因為一個float占4個字節(jié)
ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
//以本機(jī)字節(jié)順序來修改此緩沖區(qū)的字節(jié)順序
bb.order(ByteOrder.nativeOrder());
mTriangleBuffer = bb.asFloatBuffer();
//將給定float[]數(shù)據(jù)從當(dāng)前位置開始,依次寫入此緩沖區(qū)
mTriangleBuffer.put(mTriangleArray);
//設(shè)置此緩沖區(qū)的位置。如果標(biāo)記已定義并且大于新的位置,則要丟棄該標(biāo)記。
mTriangleBuffer.position(0);
//顏色相關(guān)
ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
bb2.order(ByteOrder.nativeOrder());
mColorBuffer = bb2.asFloatBuffer();
mColorBuffer.put(mColor);
mColorBuffer.position(0);
}
繪制
我們在onDrawFrame的生命周期中進(jìn)行繪制。
@Override
public void onDrawFrame(GL10 gl) {
Log.d("GLRender2" , "call onDrawFrame");
// 清除屏幕和深度緩存
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 重置當(dāng)前的模型觀察矩陣
gl.glLoadIdentity();
// 允許設(shè)置頂點
//GL10.GL_VERTEX_ARRAY頂點數(shù)組
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允許設(shè)置顏色
//GL10.GL_COLOR_ARRAY顏色數(shù)組
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
//將畫筆在z軸上移動
gl.glTranslatef(0f, 0.0f, -2.0f);
// 設(shè)置三角形
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
// 設(shè)置三角形顏色
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
// 繪制三角形
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
// 取消顏色設(shè)置
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
// 取消頂點設(shè)置
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
//繪制結(jié)束
gl.glFinish();
}
繪制的過程比較模式化,不再贅述。大致包含了:
- 清除緩存
- 啟動頂點數(shù)組模式
- 啟動顏色數(shù)組模式
- 移動畫筆
- 設(shè)置圖形
- 設(shè)置顏色
- 關(guān)閉頂點數(shù)組模式
- 關(guān)閉顏色數(shù)組模式
- 繪制結(jié)束
至此,便完成了一個平平無奇的三角形的繪制過程。
如有問題,歡迎指正。