1、概述
Android框架提供了大量標(biāo)準(zhǔn)工具戈钢,用于創(chuàng)建有吸引力的功能性圖形用戶界面痹仙。如果想要更多地控制應(yīng)用程序在屏幕上繪制的內(nèi)容,或者想要繪制三維圖形殉了,則需要使用不同的工具开仰。Android框架提供的OpenGL ES提供了一組工具,用于顯示高端動(dòng)畫(huà)圖形,并且還可以受益于許多Android設(shè)備上提供的圖形處理單元(GPU)的加速众弓。這邊主要初步使用OpenGL ES來(lái)顯示圖形并與之交互恩溅。這邊采用的OpenGL ES 2.0,使用這個(gè)版本的原因是OpenGL ES 1.x的版本和2.0基本是兩套框架谓娃,許多東西不兼容而且過(guò)時(shí)脚乡,而現(xiàn)在比較新的OpenGL ES 3.x的版本是兼容和復(fù)用2.0的接口的,所以這邊以2.0版本作為切入點(diǎn)來(lái)討論滨达。我之前有一篇ARCore的文章——ARCore 相關(guān)奶稠,里面就有提到OpenGL ES 3.x來(lái)實(shí)現(xiàn)相關(guān)AR功能。這篇文章主要討論最最基礎(chǔ)的一些OpenGL ES 2.0操作捡遍,來(lái)實(shí)現(xiàn)用OpenGL ES在Android系統(tǒng)上顯示圖形窒典,并與之交互。
2稽莉、構(gòu)建環(huán)境
要在Android應(yīng)用程序中使用OpenGL ES繪制圖形瀑志,必須為它們創(chuàng)建一個(gè)視圖容器。其中一種比較直接的方法是實(shí)現(xiàn)一個(gè) GLSurfaceView和一個(gè)GLSurfaceView.Renderer污秆。
① GLSurfaceView:一個(gè)用OpenGL ES來(lái)繪制圖片的視圖容器劈猪。這個(gè)類(lèi)是一個(gè)View可以使用OpenGL API調(diào)用繪制和操作對(duì)象的類(lèi),在功能上類(lèi)似于SurfaceView良拼。
② GLSurfaceView.Renderer:用于控制將什么內(nèi)容顯示在GLSurfaceView上战得。這是一個(gè)接口類(lèi),此接口定義了在一個(gè)GLSurfaceView中繪制圖形所需的方法庸推。必須將此接口的實(shí)現(xiàn)作為單獨(dú)的類(lèi)提供常侦,并使用GLSurfaceView.setRenderer()將其附加到GLSurfaceView實(shí)例中 。GLSurfaceView.Renderer接口要求實(shí)現(xiàn)以下方法:
onSurfaceCreated():創(chuàng)建GLSurfaceView的時(shí)候會(huì)調(diào)用一次該方法贬媒。使用此方法執(zhí)行僅需要發(fā)生一次的操作聋亡,例如設(shè)置OpenGL環(huán)境參數(shù)或初始化OpenGL圖形對(duì)象。
onDrawFrame():GLSurfaceView在每次重繪時(shí)調(diào)用此方法际乘。使用此方法作為繪制(和重新繪制)圖形對(duì)象的主要執(zhí)行點(diǎn)坡倔。
onSurfaceChanged():GLSurfaceView在幾何圖形更改時(shí)調(diào)用此方法,包括更改GLSurfaceView設(shè)備屏幕的大小或方向脖含。例如罪塔,當(dāng)設(shè)備從縱向更改為橫向時(shí),系統(tǒng)會(huì)調(diào)用此方法养葵。使用此方法響應(yīng)GLSurfaceView容器更改時(shí)需要進(jìn)行的操作征堪。
GLSurfaceView只是將OpenGL ES圖形合并到應(yīng)用程序中的一種方法。對(duì)于全屏或近全屏圖形視圖关拒,這是一個(gè)合理的選擇后裸。想要在布局的一小部分中加入OpenGL ES圖形可以使用TextureView。也可以使用構(gòu)建OpenGL ES視圖SurfaceView淮逻,這樣更靈活但這需要編寫(xiě)相當(dāng)多的額外代碼。
2.1 添加聲明
為了使應(yīng)用程序能夠使用OpenGL ES 2.0 API涌韩,必須在manifest中添加以下聲明:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果應(yīng)用程序使用紋理壓縮,則還必須聲明應(yīng)用程序支持哪種壓縮格式氯夷,以便它僅安裝在兼容設(shè)備上臣樱。
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
這邊說(shuō)到紋理壓縮指可以通過(guò)減少內(nèi)存需求和更有效地利用內(nèi)存帶寬來(lái)顯著提高OpenGL應(yīng)用程序的性能。Android框架提供對(duì)ETC1壓縮格式的支持腮考,作為標(biāo)準(zhǔn)功能雇毫。但是ETC1紋理壓縮格式不支持具有透明度(alpha通道)的紋理。如果應(yīng)用程序需要具有透明度的紋理踩蔚,則應(yīng)調(diào)查目標(biāo)設(shè)備上可用的其他紋理壓縮格式棚放。支持使用OpenGL ES 3.0 API時(shí),要保證可以使用ETC2 / EAC紋理壓縮格式馅闽。這種紋理格式提供出色的壓縮比和高視覺(jué)質(zhì)量飘蚯,格式還支持透明度(alpha通道)。而paletted是指通用的調(diào)色板紋理壓縮福也。還有ATITC(ATC)局骤、PVRTC、S3TC(DXT n / DXTC)暴凑、3DC等壓縮策略峦甩。
對(duì)于Google Play上的應(yīng)用,如果你添加了這些聲明现喳,會(huì)自動(dòng)檢測(cè)手機(jī)是否支持相關(guān)功能凯傲,如果不支持就不能下載該應(yīng)用。對(duì)于國(guó)內(nèi)的應(yīng)用商店嗦篱,不是很清楚是否會(huì)有改過(guò)濾冰单。
2.2 創(chuàng)建用于OpenGL ES圖形的Activity
使用OpenGL ES的Android應(yīng)用程序就像任何其他具有用戶界面的應(yīng)用程序一樣具有Activity。與其他應(yīng)用程序的主要區(qū)別在于在Activity的布局中添加的內(nèi)容默色。在使用OpenGL ES的應(yīng)用程序球凰,可以添加一個(gè)GLSurfaceView。
以下代碼示例顯示了使用一個(gè)GLSurfaceView作為其主視圖的Activity腿宰,當(dāng)然GLSurfaceView也可以像一般View一樣用在XML中:
public class OpenGLES20Activity extends Activity {
public static final String TAG = "OpenGLES20";
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 創(chuàng)建一個(gè)GLSurfaceView實(shí)例并將其設(shè)置為此Activity的ContentView。
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
2.3 構(gòu)建GLSurfaceView
一個(gè)GLSurfaceView是一個(gè)專(zhuān)門(mén)的視圖缘厢,可以在其中繪制OpenGL ES圖形吃度。它本身并沒(méi)有太大作用。對(duì)象的實(shí)際繪制在GLSurfaceView.Renderer中贴硫。這邊暫時(shí)可以直接使用GLSurfaceView椿每,但下面會(huì)講到的用于捕獲觸摸事件來(lái)進(jìn)行交互時(shí)候就需要擴(kuò)展這個(gè)類(lèi)了伊者。
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// 創(chuàng)建一個(gè)OpenGL ES 2.0 的context
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// 設(shè)置渲染器(Renderer)以在GLSurfaceView上繪制
setRenderer(mRenderer);
// 僅在繪圖數(shù)據(jù)發(fā)生更改時(shí)才渲染視圖
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
}
上面這邊先是設(shè)置了使用OpenGL ES 的版本,然后創(chuàng)建了一個(gè)GLSurfaceView.Renderer并將其設(shè)置為該GLSurfaceView的渲染者间护,最后設(shè)置了一下渲染模式亦渗,將其設(shè)置為RENDERMODE_WHEN_DIRTY,在該模式下當(dāng)渲染內(nèi)容變化時(shí)不會(huì)主動(dòng)刷新效果汁尺,需要手動(dòng)調(diào)用requestRender() 才行法精。
2.4 構(gòu)建渲染器(GLSurfaceView.Renderer)
Renderer這個(gè)類(lèi)前面已經(jīng)提到,需要重寫(xiě)onSurfaceCreated() 痴突、onDrawFrame() 搂蜓、onSurfaceChanged() 這三個(gè)方法,下面實(shí)現(xiàn)一個(gè)最基本的渲染器辽装,之后會(huì)再增加內(nèi)容帮碰。
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// 設(shè)置重繪背景框架顏色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// 重繪背景顏色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
// 設(shè)置渲染的位置和大小
GLES20.glViewport(0, 0, width, height);
}
}
上面的代碼示例創(chuàng)建了一個(gè)簡(jiǎn)單的OpenGL ES應(yīng)用程序,它使用OpenGL顯示黑屏拾积。
3殉挽、定義形狀
OpenGL ES 中主要能定義點(diǎn)線和三角形,而其他多邊形都是由三角形組合而成的拓巧,這邊舉一個(gè)三角形和正方形的例子斯碌。
3.1 三角形
OpenGL ES允許使用三維空間中的坐標(biāo)定義繪制對(duì)象。因此玲销,在繪制三角形之前输拇,必須定義其坐標(biāo)。在OpenGL中贤斜,執(zhí)行此操作的典型方法是為坐標(biāo)定義浮點(diǎn)數(shù)的頂點(diǎn)數(shù)組策吠。為了獲得最大效率,可以將這些坐標(biāo)寫(xiě)入一個(gè)ByteBuffer中瘩绒,然后傳入OpenGL ES圖形管道進(jìn)行處理猴抹。
public class Triangle {
private FloatBuffer vertexBuffer;
// 此數(shù)組中每個(gè)頂點(diǎn)的維度
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // 按逆時(shí)針順序
0.0f, 0.622008459f, 0.0f, // 上
-0.5f, -0.311004243f, 0.0f, // 左下
0.5f, -0.311004243f, 0.0f // 右下
};
// 設(shè)置顏色的R(紅),G(綠),B(藍(lán)),A(透明度) 值
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// 為形狀坐標(biāo)數(shù)組初始化頂點(diǎn)的字節(jié)緩沖區(qū)
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# squareCoords 數(shù)組長(zhǎng)度 * 每個(gè)float占4字節(jié))
triangleCoords.length * 4);
// 緩沖區(qū)讀取順序使用設(shè)備硬件的本地字節(jié)讀取順序
bb.order(ByteOrder.nativeOrder());
// 從ByteBuffer創(chuàng)建一個(gè)浮點(diǎn)緩沖區(qū)
vertexBuffer = bb.asFloatBuffer();
// 將坐標(biāo)點(diǎn)加到FloatBuffer中
vertexBuffer.put(triangleCoords);
// 設(shè)置緩沖區(qū)開(kāi)始讀取位置,這邊設(shè)置為從頭開(kāi)始讀取
vertexBuffer.position(0);
}
}
默認(rèn)情況下锁荔,OpenGL ES會(huì)有一個(gè)坐標(biāo)系蟀给,其中[0,0,0](X,Y阳堕,Z)指GLSurfaceView框架的中心跋理,[1,1,0]是框架的右上角,并且[-1 恬总,-1,0]是框架的左下角前普。此形狀的坐標(biāo)以逆時(shí)針順序定義。繪圖順序很重要壹堰,因?yàn)樗x了哪一面是想要繪制的形狀的正面拭卿,以及背面骡湖,在繪制時(shí)候可以根據(jù)需求控制只繪制正面或者背面或者都繪制。
3.2 正方形
在OpenGL中定義三角形如上所示峻厚,但是如果想定義一個(gè)多邊形响蕴,例如正方形。在OpenGL ES中繪制這樣一個(gè)形狀的典型途徑是使用兩個(gè)繪制在一起的三角形:
同樣惠桃,應(yīng)該以逆時(shí)針順序?yàn)楸硎敬诵螤畹膬蓚€(gè)三角形定義頂點(diǎn)浦夷,并將值放在一個(gè)ByteBuffer中。為了避免定義每個(gè)三角形共享的兩個(gè)坐標(biāo)點(diǎn)刽射,使用繪圖列表告訴OpenGL ES圖形管道如何繪制這些頂點(diǎn)军拟。
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// 此數(shù)組中每個(gè)頂點(diǎn)的坐標(biāo)數(shù)
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // 左上
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, // 右下
0.5f, 0.5f, 0.0f }; // 右上
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // 頂點(diǎn)繪制順序
public Square() {
// 為形狀坐標(biāo)數(shù)組初始化頂點(diǎn)的字節(jié)緩沖區(qū)
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# squareCoords 數(shù)組長(zhǎng)度 * 每個(gè)float占4字節(jié))
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// 為繪制順序數(shù)組 初始化字節(jié)緩沖區(qū)
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# drawOrder 數(shù)組長(zhǎng)度 * 每個(gè) short 占2字節(jié))
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
4 繪制形狀
在前面一節(jié)定義要使用OpenGL繪制的形狀后,這一節(jié)介紹如何繪制誓禁。使用OpenGL ES 2.0繪制形狀需要的代碼比較多懈息,因?yàn)锳PI提供了對(duì)圖形渲染管道的大量控制。這也是為什么說(shuō)OpenGL ES對(duì)開(kāi)發(fā)者不友好的的原因了摹恰。
4.1 初始化形狀
在進(jìn)行任何繪圖之前辫继,必須初始化并加載計(jì)劃繪制的形狀。除非在程序中使用的形狀的結(jié)構(gòu)(原始坐標(biāo))在執(zhí)行過(guò)程中發(fā)生更改俗慈,否則應(yīng)該在onSurfaceCreated()渲染器的方法中初始化它們以避免反復(fù)初始化姑宽。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// 初始化三角形
mTriangle = new Triangle();
// 初始化正方形
mSquare = new Square();
}
...
}
4.2 繪制形狀
使用OpenGL ES 2.0繪制定義的形狀需要大量代碼,因?yàn)楸仨毾驁D形渲染管道提供大量細(xì)節(jié)闺阱。具體而言炮车,必須定義以下內(nèi)容:
① 頂點(diǎn)著色器(Vertex Shader):用于渲染形狀頂點(diǎn)的OpenGL ES圖形代碼。
② 片段著色器(Fragment Shader):OpenGL ES代碼酣溃,用于渲染具有顏色或紋理的形狀的面瘦穆。
③ 程序(Program):一個(gè)OpenGL ES對(duì)象,包含要用于繪制一個(gè)或多個(gè)形狀的著色器赊豌。
需要至少一個(gè)頂點(diǎn)著色器來(lái)繪制形狀扛或,并使用一個(gè)片段著色器為該形狀著色。必須編譯這些著色器碘饼,然后將其添加到OpenGL ES程序中熙兔,然后使用該程序繪制形狀。以下是如何定義可用于在Triangle類(lèi)中繪制形狀的基本著色器的示例:
public class Triangle {
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
...
}
著色器包含OpenGL著色語(yǔ)言(GLSL)代碼艾恼,必須在OpenGL ES環(huán)境中使用它之前進(jìn)行編譯住涉。要編譯此代碼,需在渲染器類(lèi)中創(chuàng)建實(shí)用程序方法:
public static int loadShader(int type, String shaderCode){
//創(chuàng)建頂點(diǎn)著色器類(lèi)型(GLES20.GL_VERTEX_SHADER)
//或片段著色器類(lèi)型(GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// 將源代碼添加到著色器并進(jìn)行編譯
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
為了繪制形狀钠绍,必須編譯著色器代碼秆吵,將它們添加到OpenGL ES程序?qū)ο螅缓箧溄釉摮绦蛭宕取T诶L制對(duì)象的構(gòu)造函數(shù)中執(zhí)行此操作纳寂,也就是說(shuō)只執(zhí)行一次就好了。因?yàn)榫幾gOpenGL ES著色器和鏈接程序在CPU周期和處理時(shí)間方面的消耗比較大泻拦,因此應(yīng)該避免多次執(zhí)行此操作毙芜。如果在運(yùn)行時(shí)不需要修改著色器代碼的內(nèi)容,則應(yīng)在構(gòu)造器中構(gòu)建代碼争拐,使其僅創(chuàng)建一次腋粥,然后緩存以供以后使用。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// 創(chuàng)建一個(gè)空的OpenGL ES 程序
mProgram = GLES20.glCreateProgram();
// 將頂點(diǎn)著色器添加到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 將片段著色器添加到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
// 編譯鏈接OpenGL ES程序
GLES20.glLinkProgram(mProgram);
}
}
此時(shí)架曹,已準(zhǔn)備好添加繪制形狀的實(shí)際調(diào)用隘冲。使用OpenGL ES繪制形狀需要指定幾個(gè)參數(shù)來(lái)告訴渲染管道想要繪制什么以及如何繪制它。由于繪圖選項(xiàng)可能因形狀而異绑雄,因此最好讓形狀類(lèi)包含自己的繪圖邏輯展辞。創(chuàng)建draw()繪制形狀的方法。此代碼將位置和顏色值設(shè)置為形狀的頂點(diǎn)著色器和片段著色器万牺,然后執(zhí)行繪圖功能罗珍。
// Triangle.class
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; //一個(gè)頂點(diǎn)占用空間,其中每個(gè)頂點(diǎn)單維值占4字節(jié)
public void draw(float[] mvpMatrix) {
// 將程序添加到OpenGL ES環(huán)境
GLES20.glUseProgram(mProgram);
// 獲取頂點(diǎn)著色器vPosition屬性(位置)的句柄
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 啟用三角形頂點(diǎn)的句柄
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 準(zhǔn)備三角坐標(biāo)數(shù)據(jù)
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// 獲取片段著色器vColor成員(顏色)的句柄
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
//設(shè)置繪制三角形的顏色
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// 獲取形狀變換矩陣的具柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
// 將模型視圖投影矩陣傳遞給著色器
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// 繪制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用頂點(diǎn)數(shù)組
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
一旦準(zhǔn)備好所有這些代碼脚粟,繪制此對(duì)象只需要在渲染器的onDrawFrame()方法中調(diào)用draw()方法覆旱。
// MyGLRenderer.class
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
之后運(yùn)行程序會(huì)得到如下效果:
上面已經(jīng)初步將三角形顯示在屏幕上了。但明顯可以看到存在問(wèn)題核无,首先如果按照前面三角形的坐標(biāo)扣唱,按理說(shuō)應(yīng)該是一個(gè)等邊三角形。其次团南,這個(gè)三角形豎屏和橫屏拉伸方向也明顯不同噪沙。關(guān)于這個(gè)問(wèn)題的原因和解決辦法,會(huì)在下一篇文章OpenGL ES 顯示圖形(下)中進(jìn)行討論已慢。