本篇文章屬于 使用 OpenGL ES 進(jìn)行圖形繪制 這個(gè)系列的第三篇文章,主要內(nèi)容是介紹在如何在 Android 應(yīng)用中利用 OpenGL 繪制圖形的形狀。文章中所有的代碼示例都已放在 Github 上,可以去項(xiàng)目 OpenGL-ES-Learning 中查看 。
在上篇文章:OpenGL ES 定義形狀 中我們定義了 OpenGL 繪制的形狀之后,下面就一起看看如何使用 OpenGL ES 2.0 接口繪制出在 OpenGL ES 定義形狀 文章中定義的形狀。
使用 OpenGL ES 2.0 繪制圖形可能會(huì)膩比想象當(dāng)中要復(fù)雜一些,因?yàn)?Android 中保留提供了大量對(duì)于圖形渲染流程控制的 API ,就像我們?cè)诶L制自定義 View 時(shí)一樣,繪制控制的方法和參數(shù)都會(huì)很豐富。
其實(shí)在前面文章:配置 OpenGL ES 的環(huán)境 里面有提到 一個(gè)核心的類 GLSurfaceView.Renderer,它是控制 view 繪制過程的渲染器,之前文章中展示了如何使用 GLSurfaceView.Renderer 進(jìn)行繪制黑色背景的簡(jiǎn)單試驗(yàn),所以接下來的關(guān)于形狀的繪制必然少不了它的參與。
初始化形狀
在開始繪制之前,需要對(duì)繪制的圖形進(jìn)行初始化并加載。如果這些形狀結(jié)構(gòu)(原始坐標(biāo))在執(zhí)行過程不會(huì)發(fā)生變化,那么應(yīng)該在 Renderer 的 onSurfaceCreated() 方法中進(jìn)行初始化和加載,這樣可以更省內(nèi)存以及提升執(zhí)行效率。
public class MyGLRenderer2 implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// initialize a triangle
mTriangle = new Triangle();
// initialize a square
mSquare = new Square();
}
...
}
繪制形狀
使用 OpenGL ES 2.0 繪制一個(gè)定義好的形狀需要較多代碼,因?yàn)槟阈枰峁┖芏鄨D形渲染流程的細(xì)節(jié),比如:
- 頂點(diǎn)著色器(Vertex Shader):用來渲染形狀(shape)頂點(diǎn)的 OpenGL ES 代碼。
OpenGL ES 2.0 渲染管線中頂點(diǎn)著色器(Vertex Shader)取代了 OpenGL ES 1.x 渲染管線中的“變換和光照”
- 片元著色器(Fragment Shader):使用顏色或紋理(texture)渲染形狀表面(face)的 OpenGL ES 代碼。
片元著色器取代了 OpenGL ES 1.x 渲染管線中的“紋理環(huán)境和顏色求和”、“霧”以及“Alpha測(cè)試”
- 程式(Program):一個(gè) OpenGL ES 對(duì)象,包含了你希望用來繪制一個(gè)或更多圖形(shape)所要用到的著色器(shader)。
以上三個(gè),你需要至少一個(gè)頂點(diǎn)著色器(Vertex Shader)來繪制一個(gè)形狀,以及一個(gè)片元著色器(Fragment Shader)為該形狀上色。這些著色器必須被編譯然后再添加到一個(gè)OpenGL ES Program當(dāng)中,并利用這個(gè) progrem 來繪制形狀。通過編寫頂點(diǎn)及片元著色器程序,來完成一些頂點(diǎn)變換和紋理顏色計(jì)算工作,實(shí)現(xiàn)更加靈活、精細(xì)化的計(jì)算與渲染。
下面的代碼在 Triangle 類中定義了基本的著色器,我們可以利用它們繪制出一個(gè)圖形:
public class Triangle {
/**
* 頂點(diǎn)著色器代碼
* attribute變量(屬性變量)只能用于頂點(diǎn)著色器中,不能用于片元著色器。一般用該變量來表示一些頂點(diǎn)數(shù)據(jù),如:頂點(diǎn)坐標(biāo)、紋理坐標(biāo)、顏色等
* uniforms變量(一致變量)用來將數(shù)據(jù)值從應(yīng)用程其序傳遞到頂點(diǎn)著色器或者片元著色器。 該變量有點(diǎn)類似C語言中的常量(const),即該變量的值不能被shader程序修改。一般用該變量表示變換矩陣、光照參數(shù)、紋理采樣器等。
* varying變量(易變變量)是從頂點(diǎn)著色器傳遞到片元著色器的數(shù)據(jù)變量。頂點(diǎn)著色器可以使用易變變量來傳遞需要插值的顏色、法向量、紋理坐標(biāo)等任意值。 在頂點(diǎn)與片元shader程序間傳遞數(shù)據(jù)是很容易的,一般在頂點(diǎn)shader中修改varying變量值,然后片元shader中使用該值,當(dāng)然,該變量在頂點(diǎn)及片元這兩段shader程序中聲明必須是一致的。
* gl_Position 為內(nèi)建變量,表示變換后點(diǎn)的空間位置。 頂點(diǎn)著色器從應(yīng)用程序中獲得原始的頂點(diǎn)位置數(shù)據(jù),這些原始的頂點(diǎn)數(shù)據(jù)在頂點(diǎn)著色器中經(jīng)過平移、旋轉(zhuǎn)、縮放等數(shù)學(xué)變換后,生成新的頂點(diǎn)位置。新的頂點(diǎn)位置通過在頂點(diǎn)著色器中寫入gl_Position傳遞到渲染管線的后繼階段繼續(xù)處理。
*/
private final String vertexShaderCode =
"attribute vec4 vPosition;" + // 應(yīng)用程序傳入頂點(diǎn)著色器的頂點(diǎn)位置
"void main() {" +
" gl_Position = vPosition;" + // 設(shè)置此次繪制此頂點(diǎn)位置
"}";
/**
* 片元著色器代碼
*/
private final String fragmentShaderCode =
"precision mediump float;" + // 設(shè)置工作精度
"uniform vec4 vColor;" + // 應(yīng)用程序傳入著色器的顏色變量
"void main() {" +
" gl_FragColor = vColor;" + // 顏色值傳給 gl_FragColor內(nèi)建變量,完成片元的著色
"}";
...
}
關(guān)于著色器和 GLSL 語言推薦幾篇文章
OpenGL ES 入門(一)著色器簡(jiǎn)介
OpenGL Shading language學(xué)習(xí)總結(jié)
著色器(Shader)包含了OpenGL Shading Language(GLSL)代碼,它必須先被編譯然后才能在 OpenGL 環(huán)境中使用。要編譯 GLSL 代碼需要在渲染器類中創(chuàng)建一個(gè)輔助方法:
public class MyGLRenderer2 implements GLSurfaceView.Renderer
...
/**
* 加載并編譯著色器代碼
* @param type 渲染器類型 {GLES20.GL_VERTEX_SHADER, GLES20.GL_FRAGMENT_SHADER}
* @param shaderCode 渲染器代碼 GLSL
* @return
*/
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
}
要繪制圖形前,必須先編譯著色器代碼并將它們添加至一個(gè) OpenGL ES Program 對(duì)象中,然后執(zhí)行鏈接方法。
Note:編譯 OpenGL ES 著色器及鏈接操作對(duì)于 CPU 周期和處理時(shí)間而言消耗巨大,所以應(yīng)該避免重復(fù)執(zhí)行這些事情。這個(gè)操作建議在形狀類的構(gòu)造方法中調(diào)用,這樣只會(huì)執(zhí)行一次。如果在執(zhí)行期間不知道著色器的內(nèi)容,可以考慮使用一次后緩存以備后續(xù)使用。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
// 加載編譯頂點(diǎn)渲染器
int vertexShader = MyGLRenderer2.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
// 加載編譯片元渲染器
int fragmentShader = MyGLRenderer2.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
至此,你已經(jīng)完全準(zhǔn)備好添加實(shí)際的調(diào)用語句來繪制你的圖形了。使用 OpenGL ES 需要一些參數(shù)來告訴渲染流程(redering pipeline )你要繪制的內(nèi)容以及如何繪制,由于每個(gè) shape 的 drawing option 都不一樣,因此將每個(gè) shape 的繪制邏輯放到自己的類里面是一個(gè)比較好的方法。
創(chuàng)建一個(gè) draw() 方法來繪制圖形。下面的代碼為形狀的頂點(diǎn)著色器和形狀著色器設(shè)置了位置和顏色值,然后執(zhí)行繪制函數(shù):
public class Triangle {
// 繪制形狀的頂點(diǎn)數(shù)量
private static final int COORDS_PER_VERTEX = 3;
...
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
一旦完成了上述所有代碼,僅需要在渲染器的 onDrawFrame() 方法中調(diào)用 draw() 方法就可以畫出我們想要畫的對(duì)象了:
public class MyGLRenderer2 implements GLSurfaceView.Renderer {
@Override
public void onDrawFrame(GL10 gl) {
...
mTriangle.draw();
}
}
運(yùn)行這個(gè)應(yīng)用時(shí),它看上去會(huì)像是這樣:

實(shí)際操作過程中你發(fā)現(xiàn),這個(gè)三角形看上去有一些扁,另外當(dāng)你改變屏幕方向時(shí),它的形狀也會(huì)隨之改變。發(fā)生形變的原因是因?yàn)閷?duì)象的頂點(diǎn)沒有根據(jù)顯示 GLSurfaceView 的屏幕區(qū)域的長(zhǎng)寬比進(jìn)行修正。你可以使用投影(Projection)或者相機(jī)視角(Camera View)來解決這個(gè)問題。
文章中所有的代碼示例都已放在 Github 上,可以去項(xiàng)目 OpenGL-ES-Learning 中查看 。
最后,這個(gè)三角形是靜止的,這看上去有些無聊。在后續(xù)文章會(huì)讓這個(gè)形狀發(fā)生旋轉(zhuǎn),并使用一些 OpenGL ES 圖形處理流程中更加新奇的用法。