通過(guò)上篇文章的學(xué)習(xí),現(xiàn)在已經(jīng)了解到篱昔,要想在 Android 端使用 OpenGL ES 繪制圖形乌逐,必須創(chuàng)建 OpenGL ES 環(huán)境和視圖窗口逻住,具體來(lái)說(shuō)就是構(gòu)建 EGL 環(huán)境,即 OpenGL ES 和 Android 底層平臺(tái)視窗系統(tǒng)之間的接口创肥。另外 OpenGL ES 2.0 版本為可編程管線达舒,我們就可以編寫著色器程序來(lái)確定繪制內(nèi)容,即編寫 Vertex Shader 頂點(diǎn)著色器和 Fragment Shader 片元著色器叹侄。
而這些工作可以通過(guò) GLSurfaceView 非常簡(jiǎn)單的實(shí)現(xiàn)巩搏。
在介紹 GLSurfaceView 之前先來(lái)看下 Android 系統(tǒng)提供的與 OpenGL ES 相關(guān)的包:
- javax.microedition.khronos.opengles: 存放 GL 繪圖指令相關(guān)代碼
- javax.microedition.khronos.egl: 存放 EGL 管理相關(guān)代碼,包括 Display趾代、surface 等
- android.opengl: 存放 GL 輔助類塔猾,連接 OpenGL 與 Android View,Activity 等
其中 GLSurfaceView 處于 android.opengl 包中稽坤,GLSurfaceView 具有以下特性:
- 內(nèi)置 EGL 管理丈甸,自帶 GL 上下文環(huán)境和 GLThread 繪制線程
- 起到連接 OpenGL ES 與 Android 的 View 層次結(jié)構(gòu)之間的橋梁作用
- 使得 OpenGL ES 庫(kù)適應(yīng)于 Activity 生命周期
- 繼承自 SurfaceView糯俗,擁有 SurfaceView 的全部特性,繪制結(jié)果會(huì)輸出到 SurfaceView 所提供的 Surface 上
- 提供了方便使用的調(diào)試工具來(lái)跟蹤 OpenGL ES 函數(shù)調(diào)用以幫助檢查錯(cuò)誤
通過(guò) GLSurfaceView 的 setRenderer 方法可設(shè)置要渲染的效果睦擂,即 GLSurfaceView.Renderer 渲染器接口得湘,該接口方法:
- onSurfaceCreated:渲染線程開(kāi)啟時(shí)調(diào)用,可做初始化背景色顿仇、初始化紋理資源等工作
- onSurfaceChanged:窗口尺寸改變時(shí)調(diào)用淘正,通常會(huì)設(shè)置視窗范圍或投影矩陣等
- onDrawFrame:外部請(qǐng)求渲染一次就調(diào)用一次,可在此載入著色器程序臼闻、激活綁定紋理以及調(diào)用繪制
下面來(lái)看具體如何繪制一個(gè)三角形:
public class Triangle implements GLSurfaceView.Renderer {
//頂點(diǎn)著色器
private static final String vertexShaderResource =
"attribute vec3 vPosition;" +
"void main() {" +
" gl_Position = vec4(vPosition.x, vPosition.y, vPosition.z, 1.0);" +
"}";
//片段著色器
private static final String fragmentShaderResource =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
//頂點(diǎn)
private final float[] vertexCoords = new float[]{
0.0f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
};
private final float color[] = {1.0f, 0f, 0f, 1.0f}; //red
// 著色器程序
private int mProgram;
// 頂點(diǎn)坐標(biāo)數(shù)據(jù)
private FloatBuffer vertexFloatBuffer;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//設(shè)置清空屏幕后的背景色
GLES30.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
//構(gòu)建頂點(diǎn)著色器
int vertexShader = GLES30.glCreateShader(GLES30.GL_VERTEX_SHADER);
GLES30.glShaderSource(vertexShader, vertexShaderResource);
GLES30.glCompileShader(vertexShader);
//構(gòu)建片段著色器
int fragmentShader = GLES30.glCreateShader(GLES30.GL_FRAGMENT_SHADER);
GLES30.glShaderSource(fragmentShader, fragmentShaderResource);
GLES30.glCompileShader(fragmentShader);
//構(gòu)建著色器程序鸿吆,并將頂點(diǎn)著色器和片段著色器鏈接進(jìn)來(lái)
mProgram = GLES30.glCreateProgram();
GLES30.glAttachShader(mProgram, vertexShader);
GLES30.glAttachShader(mProgram, fragmentShader);
GLES30.glLinkProgram(mProgram);
//頂點(diǎn)著色器和片段著色器鏈接到著色器程序后就無(wú)用了
GLES30.glDeleteShader(vertexShader);
GLES30.glDeleteShader(fragmentShader);
//轉(zhuǎn)換為需要的頂點(diǎn)數(shù)據(jù)格式
vertexFloatBuffer = floatToBuffer(vertexCoords);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//設(shè)置視窗
GLES30.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
//清空屏幕,擦除屏幕上所有的顏色述呐,用 glClearColor 定義的顏色填充
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
//在當(dāng)前 EGL 環(huán)境激活著色器程序
GLES30.glUseProgram(mProgram);
//獲取頂點(diǎn)著色器的 vPosition 成員句柄
int positionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
//啟用句柄
GLES30.glEnableVertexAttribArray(positionHandle);
//設(shè)置頂點(diǎn)坐標(biāo)數(shù)據(jù)
GLES30.glVertexAttribPointer(positionHandle, 3, GLES30.GL_FLOAT,
false, 3 * 4, vertexFloatBuffer);
//獲取片元著色器的 vColor 成員句柄
int colorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
//設(shè)置顏色
GLES30.glUniform4fv(colorHandle, 1, color, 0);
//繪制三角形
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 3);
//禁止頂點(diǎn)數(shù)組的句柄
GLES30.glDisableVertexAttribArray(positionHandle);
}
private FloatBuffer floatToBuffer(float[] a) {
ByteBuffer buffer = ByteBuffer.allocateDirect(a.length * 4); //float占4個(gè)字節(jié)
buffer.order(ByteOrder.nativeOrder());
FloatBuffer byteBuffer = buffer.asFloatBuffer();
byteBuffer.put(a);
byteBuffer.position(0);
return byteBuffer;
}
}
頂點(diǎn)輸入
要繪制一個(gè)三角形惩淳,就要確定三個(gè)頂點(diǎn)的 3D 坐標(biāo)(OpenGL 是一個(gè) 3D 圖形庫(kù),在 OpenGL 中指定的所有坐標(biāo)都需要是 3D 坐標(biāo)乓搬,即 x思犁、y 和 z)。而頂點(diǎn)坐標(biāo)起始于局部坐標(biāo)进肯,需要為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)激蹲,即 x、y江掩、z 的范圍限定于 -1 到 1 之間学辱,任何落在范圍外的坐標(biāo)都會(huì)被丟棄。上面代碼中輸入的頂點(diǎn)數(shù)據(jù)為:
private final float[] vertexCoords = new float[]{
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
這里將 z 坐標(biāo)都設(shè)置為 0环形,表示三角形每一點(diǎn)的深度都為 0(通常深度可以理解為z坐標(biāo)策泣,它代表一個(gè)像素在空間中和你的距離,如果離你遠(yuǎn)就可能被別的像素遮擋斟赚,你就看不到它了着降,它會(huì)被丟棄,以節(jié)省資源)拗军,這樣定義的頂點(diǎn)數(shù)據(jù)反應(yīng)到標(biāo)準(zhǔn)化設(shè)備坐標(biāo)系中就是這樣的:
解釋頂點(diǎn)數(shù)據(jù)
可以看到調(diào)用 glVertexAttribPointer 設(shè)置頂點(diǎn)數(shù)據(jù)時(shí)并不是直接把 float[] 數(shù)組傳遞進(jìn)去任洞,而是轉(zhuǎn)換成 FloatBuffer 傳入,所謂的解釋頂點(diǎn)數(shù)據(jù)就是說(shuō)明輸入數(shù)據(jù)的哪一個(gè)部分對(duì)應(yīng)頂點(diǎn)著色器的哪一個(gè)頂點(diǎn)屬性发侵。對(duì)于 glVertexAttribPointer 函數(shù):
public static void glVertexAttribPointer(
int indx,
int size,
int type,
boolean normalized,
int stride,
java.nio.Buffer ptr
)
- indx:指定要配置的頂點(diǎn)屬性交掏,這里傳入頂點(diǎn)著色器的 vPosition 成員句柄
- size:指定頂點(diǎn)屬性的大小,頂點(diǎn)屬性是一個(gè) vec3刃鳄,它由 3 個(gè)值(x盅弛、y、z)組成,所以大小傳入 3
- type:指定數(shù)據(jù)的類型為 float 類型
- normalized:是否希望數(shù)據(jù)被標(biāo)準(zhǔn)化
- stride:指定連續(xù)的頂點(diǎn)數(shù)據(jù)之間的間隔挪鹏,由于一個(gè)頂點(diǎn)數(shù)據(jù)長(zhǎng)度為 3 個(gè) float见秽,所以把步長(zhǎng)設(shè)置為 3 * 4(一個(gè) float 占 4 個(gè)字節(jié))
- ptr:頂點(diǎn)數(shù)據(jù)
下圖很好的闡釋這個(gè)邏輯:
頂點(diǎn)著色器
private static final String vertexShaderResource =
"attribute vec3 vPosition;" +
"void main() {" +
" gl_Position = vec4(vPosition.x, vPosition.y, vPosition.z, 1.0);" +
"}";
由于每個(gè)頂點(diǎn)都有一個(gè) 3D 坐標(biāo),這里就創(chuàng)建一個(gè) vec3 變量輸入頂點(diǎn)坐標(biāo)讨盒。而內(nèi)置變量 gl_Position 為 vec4 類型解取,所以需要將三維向量轉(zhuǎn)換為四維向量,最后 gl_Position 設(shè)置的值會(huì)成為該頂點(diǎn)著色器的輸出返顺。
onDrawFrame 方法中在獲取頂點(diǎn)著色器的 vPosition 成員句柄后禀苦,需要調(diào)用 glEnableVertexAttribArray、glDisableVertexAttribArray 分別啟用遂鹊、禁止頂點(diǎn)數(shù)據(jù)振乏,而片段著色器的 vColor 成員句柄就不需要。這是因?yàn)槌鲇谛阅芸紤]秉扑,所有頂點(diǎn)著色器的屬性默認(rèn)都是關(guān)閉的慧邮。
glVertexAttribPointer 只是建立 CPU 和 GPU 之間的邏輯連接實(shí)現(xiàn)將 CPU 數(shù)據(jù)上傳至 GPU,但是邻储,數(shù)據(jù)在 GPU 端是否可見(jiàn)萧福,即著色器能否讀取到數(shù)據(jù)般眉,還要取決于 glEnableVertexAttribArray 方法。
片段著色器
private static final String fragmentShaderResource =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
在 OpenGL 中定義一個(gè)顏色的數(shù)據(jù)格式為 RGBA 四個(gè) 0.0 到 1.0 之間強(qiáng)度的分量赦抖,片段著色器所做的是計(jì)算像素最后的顏色輸出淘钟,也只有 gl_FragColor 這一個(gè)輸出變量宦赠。
編譯著色器
int vertexShader = GLES30.glCreateShader(GLES30.GL_VERTEX_SHADER);
GLES30.glShaderSource(vertexShader, vertexShaderResource);
GLES30.glCompileShader(vertexShader);
為了讓 OpenGL 能夠使用我們編寫的著色器源碼,必須在運(yùn)行時(shí)動(dòng)態(tài)編譯米母。首先通過(guò) glCreateShader 創(chuàng)建一個(gè)著色器對(duì)象勾扭,返回該著色器的 ID,然后通過(guò) glShaderSource铁瞒、glCompileShader 方法將源碼附著在著色器對(duì)象上并編譯它妙色。
編譯著色器可能失敗,一般編譯時(shí)會(huì)通過(guò)如下方法判斷是否編譯成功并輸出編譯信息:
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
Log.d(TAG, "glCompileStatus: " + compileStatus[0]
+ " log:" + GLES20.glGetShaderInfoLog(shaderObjectId));
if (compileStatus[0] == 0) { //編譯失敗
GLES20.glDeleteShader(shaderObjectId);
return 0;
}
著色器程序
mProgram = GLES30.glCreateProgram();
GLES30.glAttachShader(mProgram, vertexShader);
GLES30.glAttachShader(mProgram, fragmentShader);
GLES30.glLinkProgram(mProgram);
著色器程序?qū)ο笫嵌鄠€(gè)著色器合并之后并最終鏈接完成的版本慧耍,它將編譯好的頂點(diǎn)著色器和片段著色器鏈接為一個(gè)著色器程序?qū)ο笊肀妫溄雍箜旤c(diǎn)著色器和片段著色器就沒(méi)用了,可以通過(guò) glDeleteShader 刪除芍碧。就像著色器的編譯一樣煌珊,我們也可以檢測(cè)鏈接著色器程序是否失敗,并獲取相應(yīng)的日志:
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linkStatus, 0);
Log.d(TAG, "glCompileStatus:" + linkStatus[0]
+ " log:" + GLES20.glGetProgramInfoLog(mProgram));
if (linkStatus[0] == 0) { //鏈接失敗
GLES20.glDeleteProgram(mProgram);
return 0;
}
鏈接成功后泌豆,在渲染的時(shí)候通過(guò) glUseProgram 方法激活著色器程序定庵,已激活著色器程序的著色器就會(huì)在渲染時(shí)被使用,最后通過(guò) glDrawArrays 方法觸發(fā)繪制。
參考文章:
《音視頻開(kāi)發(fā)進(jìn)階指南 - 基于Android與IOS平臺(tái)的實(shí)踐》
Android GLSurfaceView詳解
GLSurfaceView
你好蔬浙,三角形
OpenGL ES 3.0 glEnableVertexAttribArray的作用